-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Megabyte Labs Automation
committed
Dec 29, 2021
1 parent
0892b34
commit 357703b
Showing
21 changed files
with
652 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
#!/usr/bin/env python | ||
|
||
# Copyright 2015 Google Inc. All Rights Reserved. | ||
# | ||
# 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. | ||
|
||
import base64 | ||
import copy | ||
import datetime | ||
import json | ||
import time | ||
import argparse | ||
|
||
# PyCrypto library: https://pypi.python.org/pypi/pycrypto | ||
from Crypto.Cipher import PKCS1_OAEP | ||
from Crypto.PublicKey import RSA | ||
from Crypto.Util.number import long_to_bytes | ||
|
||
# Google API Client Library for Python: | ||
# https://developers.google.com/api-client-library/python/start/get_started | ||
import google.auth | ||
from googleapiclient.discovery import build | ||
|
||
|
||
def GetCompute(): | ||
"""Get a compute object for communicating with the Compute Engine API.""" | ||
credentials, project = google.auth.default() | ||
compute = build("compute", "v1", credentials=credentials) | ||
return compute | ||
|
||
|
||
def GetInstance(compute, instance, zone, project): | ||
"""Get the data for a Google Compute Engine instance.""" | ||
cmd = compute.instances().get(instance=instance, project=project, zone=zone) | ||
return cmd.execute() | ||
|
||
|
||
def GetKey(): | ||
"""Get an RSA key for encryption.""" | ||
# This uses the PyCrypto library | ||
key = RSA.generate(2048) | ||
return key | ||
|
||
|
||
def GetModulusExponentInBase64(key): | ||
"""Return the public modulus and exponent for the key in bas64 encoding.""" | ||
mod = long_to_bytes(key.n) | ||
exp = long_to_bytes(key.e) | ||
|
||
modulus = base64.b64encode(mod) | ||
exponent = base64.b64encode(exp) | ||
|
||
return modulus, exponent | ||
|
||
|
||
def GetExpirationTimeString(): | ||
"""Return an RFC3339 UTC timestamp for 5 minutes from now.""" | ||
utc_now = datetime.datetime.utcnow() | ||
# These metadata entries are one-time-use, so the expiration time does | ||
# not need to be very far in the future. In fact, one minute would | ||
# generally be sufficient. Five minutes allows for minor variations | ||
# between the time on the client and the time on the server. | ||
expire_time = utc_now + datetime.timedelta(minutes=5) | ||
return expire_time.strftime("%Y-%m-%dT%H:%M:%SZ") | ||
|
||
|
||
def GetJsonString(user, modulus, exponent, email): | ||
"""Return the JSON string object that represents the windows-keys entry.""" | ||
|
||
converted_modulus = modulus.decode("utf-8") | ||
converted_exponent = exponent.decode("utf-8") | ||
|
||
expire = GetExpirationTimeString() | ||
data = { | ||
"userName": user, | ||
"modulus": converted_modulus, | ||
"exponent": converted_exponent, | ||
"email": email, | ||
"expireOn": expire, | ||
} | ||
|
||
return json.dumps(data) | ||
|
||
|
||
def UpdateWindowsKeys(old_metadata, metadata_entry): | ||
"""Return updated metadata contents with the new windows-keys entry.""" | ||
# Simply overwrites the "windows-keys" metadata entry. Production code may | ||
# want to append new lines to the metadata value and remove any expired | ||
# entries. | ||
new_metadata = copy.deepcopy(old_metadata) | ||
new_metadata["items"] = [{"key": "windows-keys", "value": metadata_entry}] | ||
return new_metadata | ||
|
||
|
||
def UpdateInstanceMetadata(compute, instance, zone, project, new_metadata): | ||
"""Update the instance metadata.""" | ||
cmd = compute.instances().setMetadata( | ||
instance=instance, project=project, zone=zone, body=new_metadata | ||
) | ||
return cmd.execute() | ||
|
||
|
||
def GetSerialPortFourOutput(compute, instance, zone, project): | ||
"""Get the output from serial port 4 from the instance.""" | ||
# Encrypted passwords are printed to COM4 on the windows server: | ||
port = 4 | ||
cmd = compute.instances().getSerialPortOutput( | ||
instance=instance, project=project, zone=zone, port=port | ||
) | ||
output = cmd.execute() | ||
return output["contents"] | ||
|
||
|
||
def GetEncryptedPasswordFromSerialPort(serial_port_output, modulus): | ||
"""Find and return the correct encrypted password, based on the modulus.""" | ||
# In production code, this may need to be run multiple times if the output | ||
# does not yet contain the correct entry. | ||
|
||
converted_modulus = modulus.decode("utf-8") | ||
|
||
output = serial_port_output.split("\n") | ||
for line in reversed(output): | ||
try: | ||
entry = json.loads(line) | ||
if converted_modulus == entry["modulus"]: | ||
return entry["encryptedPassword"] | ||
except ValueError: | ||
pass | ||
|
||
|
||
def DecryptPassword(encrypted_password, key): | ||
"""Decrypt a base64 encoded encrypted password using the provided key.""" | ||
|
||
decoded_password = base64.b64decode(encrypted_password) | ||
cipher = PKCS1_OAEP.new(key) | ||
password = cipher.decrypt(decoded_password) | ||
return password | ||
|
||
|
||
def Arguments(): | ||
# Create the parser | ||
args = argparse.ArgumentParser(description="List the content of a folder") | ||
|
||
# Add the arguments | ||
args.add_argument( | ||
"--instance", metavar="instance", type=str, help="compute instance name" | ||
) | ||
|
||
args.add_argument("--zone", metavar="zone", type=str, help="compute zone") | ||
|
||
args.add_argument("--project", metavar="project", type=str, help="gcp project") | ||
|
||
args.add_argument("--username", metavar="username", type=str, help="username") | ||
|
||
args.add_argument("--email", metavar="email", type=str, help="email") | ||
|
||
# return arguments | ||
return args.parse_args() | ||
|
||
|
||
def main(): | ||
config_args = Arguments() | ||
|
||
# Setup | ||
compute = GetCompute() | ||
key = GetKey() | ||
modulus, exponent = GetModulusExponentInBase64(key) | ||
|
||
# Get existing metadata | ||
instance_ref = GetInstance( | ||
compute, config_args.instance, config_args.zone, config_args.project | ||
) | ||
old_metadata = instance_ref["metadata"] | ||
# Create and set new metadata | ||
metadata_entry = GetJsonString( | ||
config_args.username, modulus, exponent, config_args.email | ||
) | ||
new_metadata = UpdateWindowsKeys(old_metadata, metadata_entry) | ||
|
||
# Get Serial output BEFORE the modification | ||
serial_port_output = GetSerialPortFourOutput( | ||
compute, config_args.instance, config_args.zone, config_args.project | ||
) | ||
|
||
UpdateInstanceMetadata( | ||
compute, | ||
config_args.instance, | ||
config_args.zone, | ||
config_args.project, | ||
new_metadata, | ||
) | ||
|
||
# Get and decrypt password from serial port output | ||
# Monitor changes from output to get the encrypted password as soon as it's generated, will wait for 30 seconds | ||
i = 0 | ||
new_serial_port_output = serial_port_output | ||
while i <= 20 and serial_port_output == new_serial_port_output: | ||
new_serial_port_output = GetSerialPortFourOutput( | ||
compute, config_args.instance, config_args.zone, config_args.project | ||
) | ||
i += 1 | ||
time.sleep(3) | ||
|
||
enc_password = GetEncryptedPasswordFromSerialPort(new_serial_port_output, modulus) | ||
|
||
password = DecryptPassword(enc_password, key) | ||
converted_password = password.decode("utf-8") | ||
|
||
# Display only the password | ||
print(format(converted_password)) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
--- | ||
# yamllint disable rule:line-length | ||
- name: Populate instance config dict Linux | ||
ansible.builtin.set_fact: | ||
instance_conf_dict: | ||
instance: '{{ instance_info.name }}' | ||
# eslint-disable-next-line max-len | ||
address: '{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP }}' | ||
user: "{{ lookup('env','USER') }}" | ||
port: '22' | ||
identity_file: '{{ ssh_identity_file }}' | ||
instance_os_type: '{{ molecule_yml.driver.instance_os_type }}' | ||
|
||
loop: '{{ server.results }}' | ||
loop_control: | ||
loop_var: instance_info | ||
no_log: true | ||
register: instance_conf_dict | ||
|
||
- name: Populate instance config dict Windows | ||
ansible.builtin.set_fact: | ||
instance_conf_dict: | ||
instance: '{{ instance_info.name }}' | ||
# eslint-disable-next-line max-len | ||
address: '{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP }}' | ||
user: molecule_usr | ||
password: '{{ instance_info.password }}' | ||
port: '{{ instance_info.winrm_port | default(5986) }}' | ||
winrm_transport: "{{ molecule_yml.driver.winrm_transport | default('ntlm') }}" | ||
winrm_server_cert_validation: "{{ molecule_yml.driver.winrm_server_cert_validation | default('ignore') }}" | ||
instance_os_type: '{{ molecule_yml.driver.instance_os_type }}' | ||
|
||
loop: '{{ win_instances }}' | ||
loop_control: | ||
loop_var: instance_info | ||
no_log: true | ||
register: instance_conf_dict | ||
|
||
- name: Wipe out instance config | ||
ansible.builtin.set_fact: | ||
instance_conf: {} | ||
|
||
- name: Convert instance config dict to a list | ||
ansible.builtin.set_fact: | ||
instance_conf: "{{ instance_conf_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" | ||
|
||
- name: Dump instance config | ||
ansible.builtin.copy: | ||
content: '{{ instance_conf }}' | ||
dest: '{{ molecule_instance_config }}' | ||
mode: '0600' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
--- | ||
# yamllint disable rule:line-length | ||
- name: create ssh keypair | ||
community.crypto.openssh_keypair: | ||
comment: "{{ lookup('env','USER') }} user for Molecule" | ||
path: '{{ ssh_identity_file }}' | ||
register: keypair | ||
|
||
- name: create molecule Linux instance(s) | ||
google.cloud.gcp_compute_instance: | ||
state: present | ||
name: '{{ item.name }}' | ||
machine_type: "{{ item.machine_type | default('n1-standard-1') }}" | ||
metadata: | ||
ssh-keys: "{{ lookup('env','USER') }}:{{ keypair.public_key }}" | ||
scheduling: | ||
preemptible: '{{ item.preemptible | default(false) }}' | ||
disks: | ||
- auto_delete: true | ||
boot: true | ||
initialize_params: | ||
disk_size_gb: '{{ item.disk_size_gb | default(omit) }}' | ||
source_image: "{{ item.image | default('projects/debian-cloud/global/images/family/debian-10') }}" | ||
source_image_encryption_key: | ||
raw_key: '{{ item.image_encryption_key | default(omit) }}' | ||
network_interfaces: | ||
- network: | ||
# eslint-disable-next-line max-len | ||
selfLink: "https://www.googleapis.com/compute/v1/projects/{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}/global/networks/{{ molecule_yml.driver.network_name | default('default') }}" | ||
subnetwork: | ||
# eslint-disable-next-line max-len | ||
selfLink: "https://compute.googleapis.com/compute/v1/projects/{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}/regions/{{ molecule_yml.driver.region }}/subnetworks/{{ molecule_yml.driver.subnetwork_name | default('default') }}" | ||
access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}" | ||
zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" | ||
project: '{{ gcp_project_id }}' | ||
scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" | ||
service_account_email: '{{ molecule_yml.driver.service_account_email | default (omit, true) }}' | ||
service_account_file: '{{ molecule_yml.driver.service_account_file | default (omit, true) }}' | ||
auth_kind: '{{ molecule_yml.driver.auth_kind | default(omit, true) }}' | ||
register: async_results | ||
loop: '{{ molecule_yml.platforms }}' | ||
loop_control: | ||
pause: 3 | ||
async: 7200 | ||
poll: 0 | ||
|
||
- name: Wait for instance(s) creation to complete | ||
ansible.builtin.async_status: | ||
jid: '{{ item.ansible_job_id }}' | ||
loop: '{{ async_results.results }}' | ||
register: server | ||
until: server.finished | ||
retries: 300 | ||
delay: 10 | ||
notify: | ||
- Populate instance config dict Linux | ||
- Convert instance config dict to a list | ||
- Dump instance config | ||
|
||
- name: Wait for SSH | ||
ansible.builtin.wait_for: | ||
port: 22 | ||
host: '{{ item.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else item.networkInterfaces.0.networkIP }}' | ||
search_regex: SSH | ||
delay: 10 | ||
loop: '{{ server.results }}' |
Oops, something went wrong.