Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
selfLink: "{{ gcp_snet }}"
access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}"
tags: "{{ item.tags | default(omit) }}"
labels: "{{ item.labels | default(omit) }}"
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) }}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
selfLink: "{{ gcp_snet }}"
access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}"
tags: "{{ item.tags | default(omit) }}"
labels: "{{ item.labels | default(omit) }}"
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) }}"
Expand Down
19 changes: 19 additions & 0 deletions test/gce/scenarios/label-verify/INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Google Cloud Engine driver installation guide

## Requirements

- A GCE credentials rc file

## Install

Please refer to the [Virtual environment][] documentation for
installation best practices. If not using a virtual environment, please
consider passing the widely recommended ['--user' flag][] when invoking
`pip`.

```bash
pip install 'molecule_gce'
```

[Virtual environment]: https://virtualenv.pypa.io/en/latest/
['--user' flag]: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site
59 changes: 59 additions & 0 deletions test/gce/scenarios/label-verify/create.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
ssh_identity_file: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
gcp_project_id: "{{ molecule_yml.driver.project_id | default(lookup('env', 'GCE_PROJECT_ID')) }}"

tasks:
- name: Make sure if linux or windows either specified
ansible.builtin.assert:
that:
- molecule_yml.driver.instance_os_type | lower == "linux" or molecule_yml.driver.instance_os_type | lower == "windows"
fail_msg: instance_os_type is possible only to specify linux or windows either

- name: Get network info
google.cloud.gcp_compute_network_info:
filters:
- name = "{{ molecule_yml.driver.network_name | default('default') }}"
project: "{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}"
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: my_network

- name: Get subnetwork info
google.cloud.gcp_compute_subnetwork_info:
filters:
- name = "{{ molecule_yml.driver.subnetwork_name | default('default') }}"
project: "{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}"
region: "{{ molecule_yml.driver.region }}"
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: my_subnetwork

- name: Set external access config
ansible.builtin.set_fact:
external_access_config:
- access_configs:
- name: External NAT
type: ONE_TO_NAT
when: molecule_yml.driver.external_access

- name: Include create_linux_instance tasks
ansible.builtin.include_tasks: tasks/create_linux_instance.yml
when:
- molecule_yml.driver.instance_os_type | lower == "linux"

- name: Include create_windows_instance tasks
ansible.builtin.include_tasks: tasks/create_windows_instance.yml
when:
- molecule_yml.driver.instance_os_type | lower == "windows"

handlers:
- name: Import main handler tasks
ansible.builtin.import_tasks: handlers/main.yml
38 changes: 38 additions & 0 deletions test/gce/scenarios/label-verify/destroy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"

tasks:
- name: Destroy molecule instance(s)
google.cloud.gcp_compute_instance:
name: "{{ item.name }}"
state: absent
zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}"
project: "{{ molecule_yml.driver.project_id | default(lookup('env', 'GCE_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 }}"
async: 7200
poll: 0
notify:
- Wipe out instance config
- Dump instance config

- name: Wait for instance(s) deletion to complete
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
register: server
until: server.finished
retries: 300
delay: 10
loop: "{{ async_results.results }}"

handlers:
- name: Import main handler tasks
ansible.builtin.import_tasks: handlers/main.yml
243 changes: 243 additions & 0 deletions test/gce/scenarios/label-verify/files/windows_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#!/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 argparse
import base64
import copy
import datetime
import json
import time

# 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
from googleapiclient.discovery import build

# Google API Client Library for Python:
# https://developers.google.com/api-client-library/python/start/get_started
from oauth2client.client import GoogleCredentials


def GetCompute():
"""Get a compute object for communicating with the Compute Engine API."""
credentials = GoogleCredentials.get_application_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
return None


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 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 <= 30 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(1)

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)) # noqa: T201


if __name__ == "__main__":
main()
Loading
Loading