| #!/usr/bin/env python |
| # |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """Creates and configures a GCE Windows VM for testing.""" |
| import argparse |
| import logging |
| import os |
| import subprocess |
| import textwrap |
| import time |
| import winrm |
| import yaml |
| |
| |
| THIS_DIR = os.path.dirname(os.path.realpath(__file__)) |
| GCE_IMAGE = 'windows-server-2016-dc-v20171010' |
| GCE_IMAGE_PROJECT = 'windows-cloud' |
| |
| |
| def logger(): |
| """Returns the module level logger.""" |
| return logging.getLogger(__name__) |
| |
| |
| def check_output(cmd, *args, **kwargs): |
| """subprocess.check_output with logging.""" |
| logger().debug('check_output: %s', ' '.join(cmd)) |
| return subprocess.check_output(cmd, *args, **kwargs) |
| |
| |
| def gcloud_compute(project, cmd): |
| """Runs a gcloud compute command for the given project.""" |
| return check_output(['gcloud', 'compute', '--project', project] + cmd) |
| |
| |
| def create_vm(args): |
| """Creates a VM in GCE.""" |
| logger().info('Creating VM %s.', args.name) |
| sysprep_file = os.path.join(THIS_DIR, '../infra/windows_sysprep.ps1') |
| gcloud_compute(args.project, [ |
| 'instances', 'create', args.name, |
| '--zone', args.zone, |
| '--machine-type', args.machine_type, |
| '--image-project', GCE_IMAGE_PROJECT, |
| '--image', GCE_IMAGE, |
| '--boot-disk-type', 'pd-ssd', |
| '--boot-disk-size', str(args.disk_size), |
| '--tags', 'windows', |
| '--metadata-from-file', 'sysprep-specialize-script-ps1={}'.format( |
| sysprep_file), |
| ]) |
| |
| |
| def create_firewall_rule(project, name, allow, source_ranges, target_tags): |
| """Creates a firewall rule for the given project.""" |
| logger().info('Creating %s firewall rule.', name) |
| gcloud_compute(project, [ |
| 'firewall-rules', 'create', name, |
| '--allow', allow, |
| '--source-ranges', source_ranges, |
| '--target-tags', target_tags, |
| ]) |
| |
| |
| def get_serial_port_contents(project, zone, name): |
| """Gets the serial port contents for the given machine.""" |
| return gcloud_compute( |
| project, ['instances', 'get-serial-port-output', name, '--zone', zone]) |
| |
| |
| def wait_for_activation_complete(project, zone, name): |
| """Waits for the machine to be ready to use. |
| |
| "Activation successful" will be printed after sysprep has completed and the |
| machine has rebooted. |
| """ |
| while True: |
| out = get_serial_port_contents(project, zone, name) |
| if 'Activation successful.' in out: |
| logger().info('Machine is up.') |
| return |
| else: |
| retry_time = 10 |
| logger().info( |
| 'Machine still not up. Sleeping for %s seconds.', retry_time) |
| time.sleep(retry_time) |
| |
| |
| def get_instance_info(project, zone, name): |
| """Returns the parsed result of gcloud compute instances describe.""" |
| data = gcloud_compute( |
| project, ['instances', 'describe', name, '--zone', zone]) |
| return yaml.load(data) |
| |
| |
| def reset_windows_password(project, zone, name): |
| """Resets the password and returns a tupe of (username, password).""" |
| cmd = [ |
| 'gcloud', 'compute', '--project', project, |
| 'reset-windows-password', '--zone', zone, name |
| ] |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
| proc.stdin.write('Y') |
| out, _ = proc.communicate() |
| if proc.returncode != 0: |
| raise RuntimeError('reset-windows-password failed') |
| |
| username = None |
| password = None |
| for line in out.splitlines(): |
| key, value = line.split(':') |
| key = key.strip() |
| value = value.strip() |
| |
| if key == 'username': |
| username = value |
| elif key == 'password': |
| password = value |
| |
| if username is None: |
| raise RuntimeError( |
| 'Could not find username in output:\n{}'.format(out)) |
| if password is None: |
| raise RuntimeError( |
| 'Could not find password in output:\n{}'.format(out)) |
| |
| return username, password |
| |
| |
| def test_winrm_connection(host, user, password): |
| """Checks that we can execute a basic WinRM command.""" |
| logger().info('Testing WinRM connection.') |
| url = 'https://{}:5986'.format(host) |
| session = winrm.Session( |
| url, auth=(user, password), server_cert_validation='ignore') |
| session.run_ps('echo "Hello, world!"') |
| |
| |
| def parse_args(): |
| """Parse and return command line arguments.""" |
| parser = argparse.ArgumentParser() |
| |
| parser.add_argument( |
| '--zone', default='us-west1-b', help='Zone the VM will be created in.') |
| |
| parser.add_argument( |
| '--machine-type', default='n1-standard-32', |
| help='GCE machine type. Defaults to 32 cores with 120GB RAM.') |
| |
| parser.add_argument( |
| '--disk-size', type=int, default=256, help='VM disk size.') |
| |
| parser.add_argument( |
| '-v', '--verbose', action='count', default=0, |
| help='Increase log level. Defaults to logging.WARNING.') |
| |
| parser.add_argument( |
| 'project', metavar='PROJECT', help='GCE project to use.') |
| |
| parser.add_argument( |
| 'name', metavar='NAME', help='Name to use for the instance.') |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| """Program entry point.""" |
| args = parse_args() |
| |
| log_levels = [logging.INFO, logging.DEBUG] |
| verbosity = min(args.verbose, len(log_levels) - 1) |
| log_level = log_levels[verbosity] |
| logging.basicConfig(level=log_level) |
| |
| create_vm(args) |
| out = gcloud_compute(args.project, ['firewall-rules', 'list']) |
| if 'winrm' not in out: |
| create_firewall_rule( |
| args.project, 'winrm', 'tcp:5986', '0.0.0.0/0', 'windows') |
| wait_for_activation_complete(args.project, args.zone, args.name) |
| |
| info = get_instance_info(args.project, args.zone, args.name) |
| host = info['networkInterfaces'][0]['accessConfigs'][0]['natIP'] |
| user, password = reset_windows_password(args.project, args.zone, args.name) |
| test_winrm_connection(host, user, password) |
| |
| secrets_py = os.path.join(THIS_DIR, '..', 'secrets.py') |
| logger().info('Writing connection information to %s', secrets_py) |
| with open(secrets_py, 'w') as secrets_file: |
| # Write the password as a raw string in case it contains backslashes. |
| secrets_file.write(textwrap.dedent("""\ |
| GCE_HOST = {} |
| GCE_USER = {} |
| GCE_PASS = {} |
| """.format(repr(host), repr(user), repr(password)))) |
| os.chmod(secrets_py, 0600) |
| logger().info('Setup completed successfully.') |
| logger().info('Host: %s', host) |
| logger().info('Username: %s', user) |
| logger().info('Password: %s', password) |
| |
| |
| if __name__ == '__main__': |
| main() |