
Cloud-Init позволяет клонировать виртуальные машины Windows с необходимыми сетевыми настройками (IP, маска подсети, основной шлюз) и данными авторизации. Данный метод позволяет автоматизировать процесс развертывания среды без непосредственной ее настройки при каждом развертывании. В случае с Proxmox взаимодействие Cloud-Init в связке с API или CLI дает безграничные возможности автоматизации.
Cloud–init — это, по сути, программа, которая запускается на гостевой машине при загрузке и ищет данные конфигурации для применения к гостевой системе во время инициализации.
В конце мануала мы получим шаблон виртуальной машина Windows 2019 для Proxmox, который можно клонировать с нужными настройками сети и паролем администратора без непосредственного взаимодействия с системой Windows.
Начнем процесс настройки. Сначала необходимо создать новую виртуальную машину и установить Windows 2019 по инструкции.
После того, как виртуальная машина с MS Windows 2019 на борту развернута добавим следующее оборудование на одноименной вкладке:
- Последовательный порт

2. Диск CloudInit


После стартуем виртуальную машину и подключимся к ней по RDP, настройки сети для гостевой машины были описаны тут.

Скопируем на гостевую машину и установим CloudBaseinitSetup_0_9_11_x64



Не забудьте тут указать порт COM1, который мы только, что создали во вкладке Оборудование.




C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf

[DEFAULT]
username=Administrator
groups=Администраторы
netbios_host_name_compatibility=true
inject_user_password=true
first_logon_behaviour=no
config_drive_raw_hhd=true
config_drive_cdrom=true
config_drive_vfat=true
locations=cdroom
bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe
mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\
metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService
verbose=true
debug=true
ntp_use_dhcp_config=true
real_time_clock_utc=true
ntp_enable_service=true
rdp_set_keepalive=true
enable_automatic_updates=true
logdir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\
logfile=cloudbase-init.log
default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN
logging_serial_port_settings=COM1,115200,N,8
local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\
plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin, cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin, cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin, cloudbaseinit.plugins.windows.createuser.CreateUserPlugin, cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin, cloudbaseinit.plugins.common.sshpublickeys.SetUserSSHPublicKeysPlugin, cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin, cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin, cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin
allow_reboot=true
stop_service_on_exit=false
check_latest_version=true
В каталоге C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts создадим файл 01ActivateAdministrator.py
import os,json,sys,subprocess,configparser,platform
def find_drive(file_path):
for number in range(65,91):
drive_letter = chr(number)
if os.path.exists(drive_letter+file_path):
return drive_letter+file_path
print("\n Searched file could not be found on any drive with path:" + file_path)
return False
def load_json_file(file_path,variable):
file = open(file_path)
data = json.load(file)
file.close()
return data.get(variable)
def get_administrator_status():
command = "(Get-LocalUser | Where-Object{$_.SID -like \"S-1-5-*-500\"}).Enabled"
run = subprocess.run(["powershell", "-Command", command], stdout=subprocess.PIPE, universal_newlines=True)
print("Is admin account enabled already: " + run.stdout)
return run.stdout
def get_administrator_name():
command = "(Get-LocalUser | Where-Object{$_.SID -like \"S-1-5-*-500\"}).Name"
run = subprocess.run(["powershell", "-Command", command], stdout=subprocess.PIPE, universal_newlines=True)
print("Administrator username: " + run.stdout)
return run.stdout
def enable_administrator_account():
command = "(Get-LocalUser | Where-Object{$_.SID -like \"S-1-5-*-500\"}).Name | Enable-LocalUser"
run = subprocess.run(["powershell", "-Command", command], stdout=subprocess.PIPE, universal_newlines=True)
print("\n Administrator account is activated by localscript")
return run.stdout
def is_os_64bit():
return platform.machine().endswith('64')
def get_data(variable,path):
configParser = configparser.RawConfigParser()
configParser.read(path)
data = configParser.get('DEFAULT',variable)
return data
# variables
meta_data_path = find_drive(":\OPENSTACK\LATEST\META_DATA.json")
admin_name = get_administrator_name()
# execute
if (meta_data_path) and ("admin_username" in load_json_file(meta_data_path,"meta")):
meta_data = load_json_file(meta_data_path,"meta")
meta_username = meta_data["admin_username"]
print("Meta_Data admin_username is :" + meta_username)
else:
if is_os_64bit():
conf_path = r'C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\cloudbase-init.conf'
print("System architecture is 64 bit.")
else:
conf_path = r'C:\Program Files (x86)\Cloudbase Solutions\Cloudbase-Init\conf\cloudbase-init.conf'
print("System architecture is 32 bit.")
meta_username = get_data('username', conf_path)
print("Conf username:"+meta_username)
if meta_username in admin_name and "False" in get_administrator_status():
run = enable_administrator_account()
sys.exit(1001)
else:
print("Cloud-init user is not Administrateur/Administrator or Admin account is already enabled, script aborted.")
sys.exit(0)
Создадим пользователя Administrator и добавим его в группу Администраторы



На этом настройки операционной системы Windows окончены, выключим гостевую машину и перейдем к настройкам Proxmox.

Войдем на ноду Proxmox с помощью WinSCP и перейдем в директорию:
/usr/share/perl5/PVE/API2/Qemu.pm



Найдем код:
if (defined(my $cipassword = $param->{cipassword})) {
# Same logic as in cloud-init (but with the regex fixed...)
$param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
}
И заменим на:
my $conf = PVE::QemuConfig->load_config($vmid);
my $ostype = $conf->{ostype};
if (defined(my $cipassword = $param->{cipassword})) {
# Same logic as in cloud-init (but with the regex fixed...)
if (!(PVE::QemuServer::windows_version($ostype))) {
$param->{cipassword} = PVE::Tools::encrypt_pw($cipassword)
if $cipassword !~ /^\$(?:[156]|2[ay])(\$.+){2}/;
}
}
Аналогично откроем файл для редактирования:
/usr/share/perl5/PVE/QemuServer/Cloudinit.pm
В этом модуле необходимо заменить функцию:
sub configdrive2_metadata {
my ($uuid) = @_;
return <<"EOF";
{
"uuid": "$uuid",
"network_config": { "content_path": "/content/0000" }
}
EOF
}
на
sub configdrive2_metadata {
my ($conf, $vmid, $user, $network) = @_;
my $uuid = Digest::SHA::sha1_hex($user.$network);
my $password = $conf->{cipassword};
my ($hostname, $fqdn) = get_hostname_fqdn($conf, $vmid);
my $startConfig = <<"EOF";
{
"hostname": "$hostname",
"uuid": "$uuid",
"admin_pass": "$password",
EOF
if (defined(my $keys = $conf->{sshkeys})) {
$startConfig .= " \"network_config\": { \"content_path\": \"/content/0000\" },\n";
$keys = URI::Escape::uri_unescape($keys);
$keys = [map { my $key = $_; chomp $key; $key } split(/\n/, $keys)];
$keys = [grep { /\S/ } @$keys];
$startConfig .= " \"keys\": [\n";
$startConfig .= " {\n";
my $keyCount = @$keys;
for (my $i=0; $i < $keyCount; $i++) {
# $startConfig .= " $keyCount "
if ($i == $keyCount-1){
$startConfig .= " \"key-$i\": \"".$keys->[$i]."\"\n";
} else {
$startConfig .= " \"key-$i\": \"".$keys->[$i]."\",\n";
}
}
$startConfig .= " }\n";
$startConfig .= " ]\n";
} else{
$startConfig .= " \"network_config\": { \"content_path\": \"/content/0000\" }\n";
}
$startConfig.= "}";
return $startConfig;
}
А так же заменим в этом же модуле:
if (!defined($meta_data)) {
$meta_data = configdrive2_gen_metadata($user_data, $network_data);
}
на:
if (!defined($meta_data)) {
$meta_data = configdrive2_metadata($conf, $vmid, $user_data, $network_data);
}
Далее в консоли перезагрузим: pvedaemon
systemctl restart pvedaemon.service






В консоли проверим заданные настройки cloudinit коман
qm cloudinit dump 102 user #cloud-config

После применения настроек во вкладке Cloud-Init, пробуем подключиться по RDP к серверу с заданным IP адресом и паролем администратора.