mirror of
https://github.com/budtmo/docker-android.git
synced 2026-07-31 04:07:25 +00:00
Big restructuring by moving to python
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import requests
|
||||
import signal
|
||||
import time
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
|
||||
from src.helper import convert_str_to_bool, get_env_value_or_raise
|
||||
from src.constants import DEVICE, ENV
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
EMULATOR = "emulator"
|
||||
GENY_SAAS = "geny_saas"
|
||||
GENY_AWS = "geny_aws"
|
||||
|
||||
|
||||
class Device(ABC):
|
||||
FORM_ID = "1FAIpQLSdrKWQdMh6Nt8v8NQdYvTIntohebAgqWCpXT3T9NofAoxcpkw"
|
||||
FORM_USER = "user"
|
||||
FORM_CITY = "city"
|
||||
FORM_REGION = "region"
|
||||
FORM_COUNTRY = "country"
|
||||
FORM_APP_VERSION = "app_version"
|
||||
FORM_APPIUM = "appium"
|
||||
FORM_APPIUM_ADDITIONAL_ARGS = "appium_additional_args"
|
||||
FORM_WEB_LOG = "web_log"
|
||||
FORM_WEB_VNC = "web_vnc"
|
||||
FORM_SCREEN_RESOLUTION = "screen_resolution"
|
||||
FORM_DEVICE_TYPE = "device_type"
|
||||
FORM_EMU_DEVICE = "emu_device"
|
||||
FORM_EMU_ANDROID_VERSION = "emu_android_version"
|
||||
FORM_EMU_NO_SKIN = "emu_no_skin"
|
||||
FORM_EMU_DATA_PARTITION = "emu_data_partition"
|
||||
FORM_EMU_ADDITIONAL_ARGS = "emu_additional_args"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.device_type = None
|
||||
self.interval_waiting = int(os.getenv(ENV.DEVICE_INTERVAL_WAITING, 2))
|
||||
self.user_behavior_analytics = convert_str_to_bool(os.getenv(ENV.USER_BEHAVIOR_ANALYTICS, "true"))
|
||||
self.form_field = {
|
||||
Device.FORM_USER: "entry.108751316",
|
||||
Device.FORM_CITY: "entry.2083022547",
|
||||
Device.FORM_REGION: "entry.1083141079",
|
||||
Device.FORM_COUNTRY: "entry.1946159560",
|
||||
Device.FORM_APP_VERSION: "entry.818050927",
|
||||
Device.FORM_APPIUM: "entry.181610571",
|
||||
Device.FORM_APPIUM_ADDITIONAL_ARGS: "entry.727759656",
|
||||
Device.FORM_WEB_LOG: "entry.1225589007",
|
||||
Device.FORM_WEB_VNC: "entry.2055392048",
|
||||
Device.FORM_SCREEN_RESOLUTION: "entry.709976626",
|
||||
Device.FORM_DEVICE_TYPE: "entry.207096546",
|
||||
Device.FORM_EMU_DEVICE: "entry.1960740382",
|
||||
Device.FORM_EMU_ANDROID_VERSION: "entry.671872491",
|
||||
Device.FORM_EMU_NO_SKIN: "entry.403556951",
|
||||
Device.FORM_EMU_DATA_PARTITION: "entry.1052258875",
|
||||
Device.FORM_EMU_ADDITIONAL_ARGS: "entry.57529972"
|
||||
}
|
||||
self.form_data = {}
|
||||
signal.signal(signal.SIGTERM, self.tear_down)
|
||||
|
||||
def set_status(self, current_status) -> None:
|
||||
bashrc_file = f"{os.getenv(ENV.WORK_PATH)}/device_status"
|
||||
with open(bashrc_file, "w+") as bf:
|
||||
bf.write(current_status)
|
||||
# It won't work using docker exec
|
||||
# os.environ[constants.ENV_DEVICE_STATUS] = current_status
|
||||
|
||||
def _prepare_analytics_payload(self) -> None:
|
||||
self.form_data.update({
|
||||
self.form_field[Device.FORM_USER]: f"{platform.platform()}_{platform.version().replace(' ', '_')}",
|
||||
self.form_field[Device.FORM_APP_VERSION]: os.getenv(ENV.DOCKER_ANDROID_VERSION),
|
||||
self.form_field[Device.FORM_DEVICE_TYPE]: self.device_type,
|
||||
self.form_field[Device.FORM_WEB_VNC]: convert_str_to_bool(os.getenv(ENV.WEB_VNC)),
|
||||
self.form_field[Device.FORM_WEB_LOG]: convert_str_to_bool(os.getenv(ENV.WEB_LOG)),
|
||||
self.form_field[Device.FORM_APPIUM]: convert_str_to_bool(os.getenv(ENV.APPIUM))
|
||||
})
|
||||
|
||||
try:
|
||||
res = requests.get("https://ipinfo.io")
|
||||
if res.ok:
|
||||
json_res = res.json()
|
||||
self.form_data.update({
|
||||
self.form_field[Device.FORM_CITY]: json_res[Device.FORM_CITY],
|
||||
self.form_field[Device.FORM_REGION]: json_res[Device.FORM_REGION],
|
||||
self.form_field[Device.FORM_COUNTRY]: json_res[Device.FORM_COUNTRY]
|
||||
})
|
||||
except requests.exceptions.RequestException as rer:
|
||||
self.logger.warning(rer)
|
||||
pass
|
||||
except KeyError as ke:
|
||||
self.logger.warning(ke)
|
||||
pass
|
||||
|
||||
def create(self) -> None:
|
||||
if self.user_behavior_analytics:
|
||||
self.logger.info("Sending user behavior analytics to improve the tool")
|
||||
try:
|
||||
form_url = f"https://docs.google.com/forms/d/e/{Device.FORM_ID}/formResponse"
|
||||
self._prepare_analytics_payload()
|
||||
requests.post(url=form_url, data=self.form_data)
|
||||
except requests.exceptions.RequestException as rer:
|
||||
self.logger.warning(rer)
|
||||
pass
|
||||
self.set_status(DEVICE.STATUS_CREATING)
|
||||
|
||||
def start(self) -> None:
|
||||
self.set_status(DEVICE.STATUS_STARTING)
|
||||
|
||||
def wait_until_ready(self) -> None:
|
||||
self.set_status(DEVICE.STATUS_BOOTING)
|
||||
|
||||
def reconfigure(self) -> None:
|
||||
self.set_status(DEVICE.STATUS_RECONFIGURING)
|
||||
|
||||
def keep_alive(self) -> None:
|
||||
self.set_status(DEVICE.STATUS_READY)
|
||||
self.logger.warning(f"{self.device_type} process will be kept alive to be able to get sigterm signal...")
|
||||
while True:
|
||||
time.sleep(2)
|
||||
|
||||
@abstractmethod
|
||||
def tear_down(self, *args) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Genymotion(Device):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def get_data_from_template(self, filename: str) -> dict:
|
||||
path_template_json = os.path.join(get_env_value_or_raise(ENV.GENYMOTION_TEMPLATE_PATH), filename)
|
||||
data = {}
|
||||
if os.path.isfile(path_template_json):
|
||||
try:
|
||||
self.logger.info(path_template_json)
|
||||
with open(path_template_json, "r") as f:
|
||||
data = json.load(f)
|
||||
except FileNotFoundError as fnf:
|
||||
self.shutdown_and_logout()
|
||||
self.logger.error(f"File cannot be found: {fnf}")
|
||||
except json.JSONDecodeError as jde:
|
||||
self.shutdown_and_logout()
|
||||
self.logger.error(f"Error Decoding Json: {jde}")
|
||||
except Exception as e:
|
||||
self.shutdown_and_logout()
|
||||
self.logger.error(e)
|
||||
else:
|
||||
self.shutdown_and_logout()
|
||||
raise RuntimeError(f"'{path_template_json}' cannot be found!")
|
||||
return data
|
||||
|
||||
@abstractmethod
|
||||
def login(self) -> None:
|
||||
pass
|
||||
|
||||
def create(self) -> None:
|
||||
super().create()
|
||||
self.login()
|
||||
|
||||
@abstractmethod
|
||||
def shutdown_and_logout(self) -> None:
|
||||
pass
|
||||
|
||||
def tear_down(self, *args) -> None:
|
||||
self.shutdown_and_logout()
|
||||
@@ -0,0 +1,237 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from src.device import Device, DeviceType
|
||||
from src.helper import convert_str_to_bool, get_env_value_or_raise, symlink_force
|
||||
from src.constants import ENV, UTF8
|
||||
|
||||
|
||||
class Emulator(Device):
|
||||
DEVICE = (
|
||||
"Nexus 4",
|
||||
"Nexus 5",
|
||||
"Nexus 7",
|
||||
"Nexus One",
|
||||
"Nexus S",
|
||||
"Samsung Galaxy S6",
|
||||
"Samsung Galaxy S7",
|
||||
"Samsung Galaxy S7 Edge",
|
||||
"Samsung Galaxy S8",
|
||||
"Samsung Galaxy S9",
|
||||
"Samsung Galaxy S10"
|
||||
)
|
||||
|
||||
API_LEVEL = {
|
||||
"9.0": "28",
|
||||
"10.0": "29",
|
||||
"11.0": "30",
|
||||
"12.0": "32",
|
||||
"13.0": "33"
|
||||
}
|
||||
|
||||
adb_name_id = 5554
|
||||
|
||||
class ReadinessCheck(Enum):
|
||||
BOOTED = "booted"
|
||||
RUN_STATE = "in running state"
|
||||
WELCOME_SCREEN = "in welcome screen"
|
||||
POP_UP_WINDOW = "pop up window"
|
||||
|
||||
def __init__(self, name: str, device: str, android_version: str, data_partition: str,
|
||||
additional_args: str, img_type: str, sys_img: str) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.adb_name = f"emulator-{Emulator.adb_name_id}"
|
||||
self.device_type = DeviceType.EMULATOR.value
|
||||
self.name = name
|
||||
if device in self.DEVICE:
|
||||
self.device = device
|
||||
else:
|
||||
raise RuntimeError(f"device '{device}' is not supported!")
|
||||
if android_version in self.API_LEVEL.keys():
|
||||
self.android_version = android_version
|
||||
else:
|
||||
raise RuntimeError(f"android version '{android_version}' is not supported!")
|
||||
self.api_level = self.API_LEVEL[self.android_version]
|
||||
self.data_partition = data_partition
|
||||
self.additional_args = additional_args
|
||||
self.img_type = img_type
|
||||
self.sys_img = sys_img
|
||||
workdir = get_env_value_or_raise(ENV.WORK_PATH)
|
||||
self.path_device_profile_target = os.path.join(workdir, ".android", "devices.xml")
|
||||
self.path_emulator = os.path.join(workdir, "emulator")
|
||||
self.path_emulator_config = os.path.join(workdir, "emulator", "config.ini")
|
||||
self.path_emulator_profiles = os.path.join(workdir, "docker-android", "mixins",
|
||||
"configs", "devices", "profiles")
|
||||
self.path_emulator_skins = os.path.join(workdir, "docker-android", "mixins",
|
||||
"configs", "devices", "skins")
|
||||
self.file_name = self.device.replace(" ", "_").lower()
|
||||
self.no_skin = convert_str_to_bool(os.getenv(ENV.EMULATOR_NO_SKIN))
|
||||
self.interval_after_booting = 15
|
||||
Emulator.adb_name_id += 2
|
||||
self.form_data.update({
|
||||
self.form_field[Device.FORM_SCREEN_RESOLUTION]: f"{os.getenv(ENV.SCREEN_WIDTH)}x"
|
||||
f"{os.getenv(ENV.SCREEN_HEIGHT)}x"
|
||||
f"{os.getenv(ENV.SCREEN_DEPTH)}",
|
||||
self.form_field[Device.FORM_EMU_DEVICE]: self.device,
|
||||
self.form_field[Device.FORM_EMU_ANDROID_VERSION]: self.android_version,
|
||||
self.form_field[Device.FORM_EMU_NO_SKIN]: self.no_skin,
|
||||
self.form_field[Device.FORM_EMU_DATA_PARTITION]: self.data_partition,
|
||||
self.form_field[Device.FORM_EMU_ADDITIONAL_ARGS]: self.additional_args
|
||||
})
|
||||
|
||||
def is_initialized(self) -> bool:
|
||||
import re
|
||||
if os.path.exists(self.path_emulator_config):
|
||||
self.logger.info("Config file exists")
|
||||
with open(self.path_emulator_config, 'r') as f:
|
||||
if any(re.match(r'hw\.device\.name ?= ?{}'.format(self.device), line) for line in f):
|
||||
self.logger.info("Selected device is already created")
|
||||
return True
|
||||
else:
|
||||
self.logger.info("Selected device is not created")
|
||||
return False
|
||||
|
||||
self.logger.info("Config file does not exist")
|
||||
return False
|
||||
|
||||
def _add_profile(self) -> None:
|
||||
if "samsung" in self.device.lower():
|
||||
path_device_profile_source = os.path.join(self.path_emulator_profiles,
|
||||
"{fn}.xml".format(fn=self.file_name))
|
||||
symlink_force(path_device_profile_source, self.path_device_profile_target)
|
||||
self.logger.info("Samsung device profile is linked")
|
||||
|
||||
def _add_skin(self) -> None:
|
||||
device_skin_path = os.path.join(
|
||||
self.path_emulator_skins, "{fn}".format(fn=self.file_name))
|
||||
with open(self.path_emulator_config, "a") as cf:
|
||||
cf.write("hw.keyboard=yes\n")
|
||||
cf.write("disk.dataPartition.size={dp}\n".format(dp=self.data_partition))
|
||||
cf.write("skin.path={sp}\n".format(
|
||||
sp="_no_skin" if self.no_skin else device_skin_path))
|
||||
self.logger.info(f"Skin is added in: '{self.path_emulator_config}'")
|
||||
|
||||
def create(self) -> None:
|
||||
super().create()
|
||||
first_run = not self.is_initialized()
|
||||
if first_run:
|
||||
self.logger.info(f"Creating the {self.device_type}...")
|
||||
self._add_profile()
|
||||
creation_cmd = "avdmanager create avd -f -n {n} -b {it}/{si} " \
|
||||
"-k 'system-images;android-{al};{it};{si}' " \
|
||||
"-d {d} -p {pe}".format(n=self.name, it=self.img_type, si=self.sys_img,
|
||||
al=self.api_level, d=self.device.replace(" ", "\ "),
|
||||
pe=self.path_emulator)
|
||||
self.logger.info(f"Command to create emulator: '{creation_cmd}'")
|
||||
subprocess.check_call(creation_cmd, shell=True)
|
||||
self._add_skin()
|
||||
self.logger.info(f"{self.device_type} is created!")
|
||||
|
||||
def change_permission(self) -> None:
|
||||
kvm_path = "/dev/kvm"
|
||||
if os.path.exists(kvm_path):
|
||||
cmds = (f"sudo chown 1300:1301 {kvm_path}",
|
||||
"sudo sed -i '1d' /etc/passwd")
|
||||
for c in cmds:
|
||||
subprocess.check_call(c, shell=True)
|
||||
self.logger.info("KVM permission is granted!")
|
||||
else:
|
||||
raise RuntimeError("/dev/kvm cannot be found!")
|
||||
|
||||
def deploy(self):
|
||||
self.logger.info(f"Deploying the {self.device_type}")
|
||||
|
||||
basic_cmd = "emulator @{n}".format(n=self.name)
|
||||
basic_args = "-gpu swiftshader_indirect -accel on -writable-system -verbose"
|
||||
wipe_arg = "-wipe-data" if not self.is_initialized() else ""
|
||||
|
||||
start_cmd = f"{basic_cmd} {basic_args} {wipe_arg} {self.additional_args}"
|
||||
self.logger.info(f"Command to run {self.device_type}: '{start_cmd}'")
|
||||
subprocess.Popen(start_cmd.split())
|
||||
|
||||
def start(self) -> None:
|
||||
super().start()
|
||||
self.change_permission()
|
||||
self.deploy()
|
||||
|
||||
def check_adb_command(self, readiness_check_type: ReadinessCheck, bash_command: str,
|
||||
expected_keyword: str, max_attempts: int, interval_waiting_time: int,
|
||||
adb_action: str = None) -> None:
|
||||
success = False
|
||||
for _ in range(1, max_attempts):
|
||||
if success:
|
||||
break
|
||||
else:
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
bash_command.split()).decode(UTF8)
|
||||
if expected_keyword in str(output).lower():
|
||||
if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
|
||||
subprocess.check_call(adb_action, shell=True)
|
||||
else:
|
||||
self.logger.info(
|
||||
f"{self.device_type} is {readiness_check_type.value}!")
|
||||
success = True
|
||||
else:
|
||||
self.logger.info(f"[attempt: {_}] {self.device_type} is not {readiness_check_type.value}! "
|
||||
f"will check again in {interval_waiting_time} seconds")
|
||||
time.sleep(interval_waiting_time)
|
||||
except subprocess.CalledProcessError:
|
||||
self.logger.warning("command cannot be executed! will continue...")
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
|
||||
self.logger.info(f"Pop up windows '{expected_keyword}' is not found!")
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"{readiness_check_type.value} is checked {_} times!")
|
||||
|
||||
def wait_until_ready(self) -> None:
|
||||
super().wait_until_ready()
|
||||
booting_cmd = f"adb -s {self.adb_name} wait-for-device shell getprop sys.boot_completed"
|
||||
focus_cmd = f"adb -s {self.adb_name} shell dumpsys window | grep -i mCurrentFocus"
|
||||
self.check_adb_command(self.ReadinessCheck.BOOTED,
|
||||
booting_cmd, "1", 60, self.interval_waiting)
|
||||
time.sleep(self.interval_after_booting)
|
||||
|
||||
interval_pop_up = 0
|
||||
max_attempt_pop_up = 3
|
||||
pop_up_system_ui = "Not Responding: com.android.systemui"
|
||||
system_ui_cmd = f"adb shell su root 'kill $(pidof com.android.systemui)'"
|
||||
pop_up_key_enter = {
|
||||
"Not Responding: com.google.android.gms",
|
||||
"Not Responding: system",
|
||||
"ConversationListActivity"
|
||||
}
|
||||
key_enter_cmd = "adb shell input keyevent KEYCODE_ENTER"
|
||||
self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pop_up_system_ui,
|
||||
max_attempt_pop_up, interval_pop_up, system_ui_cmd)
|
||||
for pe in pop_up_key_enter:
|
||||
self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pe, max_attempt_pop_up,
|
||||
interval_pop_up, key_enter_cmd)
|
||||
|
||||
self.check_adb_command(self.ReadinessCheck.WELCOME_SCREEN,
|
||||
focus_cmd, "launcheractivity", 60, self.interval_waiting)
|
||||
self.logger.info(f"{self.device_type} is ready to use")
|
||||
|
||||
def tear_down(self, *args) -> None:
|
||||
self.logger.warning("Sigterm is detected! Nothing to do!")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
return "Emulator(name={n}, device={d}, adb_name={an}, android_version={av}, api_level={al}, " \
|
||||
"data_partition={dp}, additional_args={aa}, img_type={it}, sys_img={si}, " \
|
||||
"path_device_profile_target={pdpt}, path_emulator={pe}, path_emulator_config={pec}, " \
|
||||
"file={f})".format(n=self.name, d=self.device, an=self.adb_name, av=self.android_version,
|
||||
al=self.api_level, dp=self.data_partition, aa=self.additional_args,
|
||||
it=self.img_type, si=self.sys_img, pdpt=self.path_device_profile_target,
|
||||
pe=self.path_emulator, pec=self.path_emulator_config, f=self.file_name)
|
||||
except AttributeError as ae:
|
||||
self.logger.error(ae)
|
||||
return ""
|
||||
@@ -0,0 +1,223 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from src.device import Genymotion, DeviceType
|
||||
from src.helper import get_env_value_or_raise
|
||||
from src.constants import ENV, UTF8
|
||||
|
||||
|
||||
class GenyAWS(Genymotion):
|
||||
port = 5555
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.device_type = DeviceType.GENY_AWS.value
|
||||
self.workdir = get_env_value_or_raise(ENV.WORK_PATH)
|
||||
self.aws_credentials_path = os.path.join(self.workdir, ".aws")
|
||||
self.remove_cred_at_the_end = False # for logout
|
||||
self.geny_aws_template_path = os.path.join(self.workdir, "docker-android", "mixins",
|
||||
"templates", "genymotion", "aws")
|
||||
self.created_devices = {}
|
||||
|
||||
def login(self) -> None:
|
||||
aws_credentials_file = os.path.join(self.aws_credentials_path, "credentials")
|
||||
if os.path.exists(self.aws_credentials_path):
|
||||
self.logger.info(".aws is found! It will be used as credentials")
|
||||
else:
|
||||
self.logger.info(".aws cannot be found! the template will be used!")
|
||||
self.remove_cred_at_the_end = True
|
||||
aws_credentials_template_path = os.path.join(self.geny_aws_template_path, ".aws")
|
||||
shutil.move(aws_credentials_template_path, self.aws_credentials_path)
|
||||
|
||||
aws_key_id = get_env_value_or_raise(ENV.AWS_ACCESS_KEY_ID)
|
||||
aws_secret_key = get_env_value_or_raise(ENV.AWS_SECRET_ACCESS_KEY)
|
||||
replacements_cred = {
|
||||
f"<{ENV.AWS_ACCESS_KEY_ID.lower()}>": aws_key_id,
|
||||
f"<{ENV.AWS_SECRET_ACCESS_KEY.lower()}>": aws_secret_key
|
||||
}
|
||||
with open(aws_credentials_file, 'r+') as cred_file:
|
||||
cred_file_contents = cred_file.read()
|
||||
for old_str, new_str in replacements_cred.items():
|
||||
cred_file_contents = cred_file_contents.replace(old_str, new_str)
|
||||
cred_file.seek(0)
|
||||
cred_file.write(cred_file_contents)
|
||||
cred_file.truncate()
|
||||
self.logger.info("aws credentials is set!")
|
||||
|
||||
def create_ssh_key(self) -> None:
|
||||
subprocess.check_call('ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""', shell=True)
|
||||
self.logger.info("ssh key is created!")
|
||||
|
||||
def create_tf_files(self) -> None:
|
||||
try:
|
||||
for item in self.get_data_from_template(ENV.GENY_AWS_TEMPLATE_FILE_NAME):
|
||||
name = item["name"]
|
||||
region = item["region"]
|
||||
ami = item["ami"]
|
||||
instance_type = item["instance_type"]
|
||||
if "security_group" in item:
|
||||
sg = item["security_group"]
|
||||
tf_content = f'''
|
||||
provider "aws" {{
|
||||
alias = "provider_{name}"
|
||||
region = "{region}"
|
||||
}}
|
||||
|
||||
resource "aws_key_pair" "geny_key_{name}" {{
|
||||
provider = aws.provider_{name}
|
||||
public_key = file("~/.ssh/id_rsa.pub")
|
||||
}}
|
||||
|
||||
resource "aws_instance" "geny_aws_{name}" {{
|
||||
provider = aws.provider_{name}
|
||||
ami = "{ami}"
|
||||
instance_type = "{instance_type}"
|
||||
vpc_security_group_ids = ["{sg}"]
|
||||
key_name = aws_key_pair.geny_key_{name}.key_name
|
||||
tags = {{
|
||||
Name = "DockerAndroid-GenyAWS-{ami}.id}}"
|
||||
}}
|
||||
|
||||
provisioner "remote-exec" {{
|
||||
connection {{
|
||||
type = "ssh"
|
||||
user = "shell"
|
||||
host = self.public_ip
|
||||
private_key = file("~/.ssh/id_rsa")
|
||||
}}
|
||||
script = "/home/androidusr/docker-android/mixins/scripts/genymotion/aws/enable_adb.sh"
|
||||
}}
|
||||
}}
|
||||
|
||||
output "public_dns_{name}" {{
|
||||
value = aws_instance.geny_aws_{name}.public_dns
|
||||
}}
|
||||
'''
|
||||
else:
|
||||
ingress_rules = json.dumps(item["ingress_rules"])
|
||||
egress_rules = json.dumps(item["egress_rules"])
|
||||
tf_content = f'''
|
||||
locals {{
|
||||
ingress_rules = {ingress_rules}
|
||||
egress_rules = {egress_rules}
|
||||
}}
|
||||
|
||||
provider "aws" {{
|
||||
alias = "provider_{name}"
|
||||
region = "{region}"
|
||||
}}
|
||||
|
||||
resource "aws_security_group" "geny_sg_{name}" {{
|
||||
provider = aws.provider_{name}
|
||||
dynamic "ingress" {{
|
||||
for_each = local.ingress_rules
|
||||
content {{
|
||||
from_port = ingress.value.from_port
|
||||
to_port = ingress.value.to_port
|
||||
protocol = ingress.value.protocol
|
||||
cidr_blocks = ingress.value.cidr_blocks
|
||||
}}
|
||||
}}
|
||||
|
||||
dynamic "egress" {{
|
||||
for_each = local.egress_rules
|
||||
content {{
|
||||
from_port = egress.value.from_port
|
||||
to_port = egress.value.to_port
|
||||
protocol = egress.value.protocol
|
||||
cidr_blocks = egress.value.cidr_blocks
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
resource "aws_key_pair" "geny_key_{name}" {{
|
||||
provider = aws.provider_{name}
|
||||
public_key = file("~/.ssh/id_rsa.pub")
|
||||
}}
|
||||
|
||||
resource "aws_instance" "geny_aws_{name}" {{
|
||||
provider = aws.provider_{name}
|
||||
ami = "{ami}"
|
||||
instance_type = "{instance_type}"
|
||||
vpc_security_group_ids = [aws_security_group.geny_sg_{name}.name]
|
||||
key_name = aws_key_pair.geny_key_{name}.key_name
|
||||
tags = {{
|
||||
Name = "DockerAndroid-GenyAWS-{ami}.id}}"
|
||||
}}
|
||||
|
||||
provisioner "remote-exec" {{
|
||||
connection {{
|
||||
type = "ssh"
|
||||
user = "shell"
|
||||
host = self.public_ip
|
||||
private_key = file("~/.ssh/id_rsa")
|
||||
}}
|
||||
script = "/home/androidusr/docker-android/mixins/scripts/genymotion/aws/enable_adb.sh"
|
||||
}}
|
||||
}}
|
||||
|
||||
output "public_dns_{name}" {{
|
||||
value = aws_instance.geny_aws_{name}.public_dns
|
||||
}}
|
||||
'''
|
||||
tf_deployment_filename = f"{name}.tf"
|
||||
self.created_devices[name] = GenyAWS.port
|
||||
GenyAWS.port += 1
|
||||
with open(tf_deployment_filename, "w") as df:
|
||||
df.write(tf_content)
|
||||
self.logger.info("Terraform files are created!")
|
||||
except Exception as e:
|
||||
self.logger.error(e)
|
||||
self.shutdown_and_logout()
|
||||
|
||||
def deploy_tf(self) -> None:
|
||||
try:
|
||||
cmds = (
|
||||
"terraform init",
|
||||
"terraform plan",
|
||||
"terraform apply -auto-approve"
|
||||
)
|
||||
for c in cmds:
|
||||
subprocess.check_call(c, shell=True)
|
||||
self.logger.info("Genymotion-Device(s) are deployed on AWS")
|
||||
except subprocess.CalledProcessError as cpe:
|
||||
self.logger.error(cpe)
|
||||
self.shutdown_and_logout()
|
||||
|
||||
def connect_with_local_adb(self) -> None:
|
||||
self.logger.info(f"created devices: {self.created_devices}")
|
||||
try:
|
||||
for d, p in self.created_devices.items():
|
||||
dns_cmd = f"terraform output public_dns_{d}"
|
||||
dns_ip = subprocess.check_output(dns_cmd.split()).decode(UTF8).replace('"', '')
|
||||
tunnel_cmd = f"ssh -i ~/.ssh/id_rsa -o ServerAliveInterval=60 -o StrictHostKeyChecking=no -q -NL " \
|
||||
f"{p}:localhost:5555 shell@{dns_ip}"
|
||||
subprocess.Popen(tunnel_cmd.split())
|
||||
time.sleep(10)
|
||||
subprocess.check_call(f"adb connect localhost:{p} >/dev/null 2>&1", shell=True)
|
||||
except Exception as e:
|
||||
self.logger.error(e)
|
||||
self.shutdown_and_logout()
|
||||
|
||||
def create(self) -> None:
|
||||
super().create()
|
||||
self.create_ssh_key()
|
||||
self.create_tf_files()
|
||||
self.deploy_tf()
|
||||
self.connect_with_local_adb()
|
||||
|
||||
def shutdown_and_logout(self) -> None:
|
||||
try:
|
||||
subprocess.check_call("terraform destroy -auto-approve -lock=false", shell=True)
|
||||
self.logger.info("device(s) is successfully removed!")
|
||||
except subprocess.CalledProcessError as cpe:
|
||||
self.logger.error(cpe)
|
||||
finally:
|
||||
if self.remove_cred_at_the_end:
|
||||
subprocess.check_call(f"rm -rf {self.aws_credentials_path}", shell=True)
|
||||
self.logger.info("successfully logged out!")
|
||||
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from src.device import Genymotion, DeviceType
|
||||
from src.helper import get_env_value_or_raise
|
||||
from src.constants import ENV, UTF8
|
||||
|
||||
|
||||
class GenySAAS(Genymotion):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.device_type = DeviceType.GENY_SAAS.value
|
||||
self.created_devices = []
|
||||
|
||||
def login(self) -> None:
|
||||
user = get_env_value_or_raise(ENV.GENY_SAAS_USER)
|
||||
password = get_env_value_or_raise(ENV.GENY_SAAS_PASS)
|
||||
subprocess.check_call(f"gmsaas auth login {user} {password} > /dev/null 2>&1", shell=True)
|
||||
self.logger.info("successfully logged in!")
|
||||
|
||||
def create(self) -> None:
|
||||
super().create()
|
||||
for item in self.get_data_from_template(ENV.GENY_SAAS_TEMPLATE_FILE_NAME):
|
||||
name = ""
|
||||
template = ""
|
||||
local_port = ""
|
||||
|
||||
# implement like this because local_port param is not a must
|
||||
for k, v in item.items():
|
||||
if k.lower() == "name":
|
||||
name = v
|
||||
elif k.lower() == "template":
|
||||
template = v
|
||||
elif k.lower() == "local_port":
|
||||
local_port = v
|
||||
else:
|
||||
self.logger.warning(f"'{k}' is not supported! Please check the documentation!")
|
||||
|
||||
if not name:
|
||||
import uuid
|
||||
name = str(uuid.uuid4())
|
||||
|
||||
if not template:
|
||||
self.shutdown_and_logout()
|
||||
raise RuntimeError(f"'template' is a must parameter and not given!")
|
||||
else:
|
||||
self.logger.info(f"name: {name}, template: {template}")
|
||||
creation_cmd = f"gmsaas instances start {template} {name}"
|
||||
try:
|
||||
instance_id = subprocess.check_output(creation_cmd.split()).decode(UTF8).replace("\n", "")
|
||||
created_device = {f"{name}": {instance_id}}
|
||||
self.created_devices.append(created_device)
|
||||
additional_args = ""
|
||||
if local_port:
|
||||
additional_args = f"--adb-serial-port {local_port}"
|
||||
connect_cmd = f"gmsaas instances adbconnect {instance_id} {additional_args}"
|
||||
subprocess.check_call(f"{connect_cmd}", shell=True)
|
||||
except Exception as e:
|
||||
self.shutdown_and_logout()
|
||||
self.logger.error(e)
|
||||
exit(1)
|
||||
|
||||
def shutdown_and_logout(self) -> None:
|
||||
if bool(self.created_devices):
|
||||
self.logger.info("Created device(s) will be removed!")
|
||||
for d in self.created_devices:
|
||||
for n, i in d.items():
|
||||
subprocess.check_call(f"gmsaas instances stop {i}", shell=True)
|
||||
self.logger.info(f"device '{n}' is successfully removed!")
|
||||
subprocess.check_call("gmsaas auth logout", shell=True)
|
||||
self.logger.info("successfully logged out!")
|
||||
Reference in New Issue
Block a user