Refactoring proxy lifetime to only shutdown when proxy is out-of-date. (#839)

## Summary of the Pull Request

_What is this about?_
We'd like to refactor the proxy lifecycle to only delete when the proxy is out-of-date - i.e. when the proxy is older than 7 days or a mismatched version. I've changed two files, proxy.py and timer_daily\init.py to check for the version and timestamp before stopping a live proxy. 

## PR Checklist
* [ ] Applies to work item: #xxx
* [ ] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/onefuzz) and sign the CLI.
* [ ] Tests added/passed
* [ ] Requires documentation to be updated
* [x] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

## Info on Pull Request

_What does this include?_
Changes to two files: 
proxy.py: 
- get_or_create() edited to check if timestamp is >7 days.
- Created is_outdated() to check version and timestamp for out-of-date proxy. 
timer_daily/init.py
- Proxy check now includes is_outdated() before determining if a proxy should be shutdown. 

## Validation Steps Performed
Deploying test instance to determine if proxy lives past a single day.
This commit is contained in:
nharper285
2021-05-20 07:33:29 -07:00
committed by GitHub
parent 2b67c7b02f
commit 2f81c44f01
13 changed files with 156 additions and 37 deletions

View File

@ -7,6 +7,7 @@ import datetime
import logging
import os
from typing import List, Optional, Tuple
from uuid import UUID, uuid4
from azure.mgmt.compute.models import VirtualMachine
from onefuzztypes.enums import ErrorCode, VmState
@ -37,12 +38,17 @@ from .proxy_forward import ProxyForward
PROXY_SKU = "Standard_B2s"
PROXY_IMAGE = "Canonical:UbuntuServer:18.04-LTS:latest"
PROXY_LOG_PREFIX = "scaleset-proxy: "
PROXY_LIFESPAN = datetime.timedelta(days=7)
# This isn't intended to ever be shared to the client, hence not being in
# onefuzztypes
class Proxy(ORMMixin):
timestamp: Optional[datetime.datetime] = Field(alias="Timestamp")
created_timestamp: datetime.datetime = Field(
default_factory=datetime.datetime.utcnow
)
proxy_id: UUID = Field(default_factory=uuid4)
region: Region
state: VmState = Field(default=VmState.init)
auth: Authentication = Field(default_factory=build_auth)
@ -50,14 +56,15 @@ class Proxy(ORMMixin):
error: Optional[Error]
version: str = Field(default=__version__)
heartbeat: Optional[ProxyHeartbeat]
outdated: bool = Field(default=False)
@classmethod
def key_fields(cls) -> Tuple[str, Optional[str]]:
return ("region", None)
return ("region", "proxy_id")
def get_vm(self) -> VM:
vm = VM(
name="proxy-%s" % self.region,
name="proxy-%s-%s" % (self.region, self.proxy_id),
region=self.region,
sku=PROXY_SKU,
image=PROXY_IMAGE,
@ -104,7 +111,9 @@ class Proxy(ORMMixin):
return
logging.error(PROXY_LOG_PREFIX + "vm failed: %s - %s", self.region, error)
send_event(EventProxyFailed(region=self.region, error=error))
send_event(
EventProxyFailed(region=self.region, proxy_id=self.proxy_id, error=error)
)
self.error = error
self.state = VmState.stopping
self.save()
@ -131,7 +140,7 @@ class Proxy(ORMMixin):
return
self.ip = ip
extensions = proxy_manager_extensions(self.region)
extensions = proxy_manager_extensions(self.region, self.proxy_id)
result = vm.add_extensions(extensions)
if isinstance(result, Error):
self.set_failed(result)
@ -154,6 +163,29 @@ class Proxy(ORMMixin):
logging.info(PROXY_LOG_PREFIX + "removing proxy: %s", self.region)
self.delete()
def is_outdated(self) -> bool:
if self.version != __version__:
logging.info(
PROXY_LOG_PREFIX + "mismatch version: proxy:%s service:%s state:%s",
self.version,
__version__,
self.state,
)
return True
if self.created_timestamp is not None:
proxy_timestamp = self.created_timestamp
if proxy_timestamp < (
datetime.datetime.now(tz=datetime.timezone.utc) - PROXY_LIFESPAN
):
logging.info(
PROXY_LOG_PREFIX
+ "proxy older than 7 days:proxy-created:%s state:%s",
self.created_timestamp,
self.state,
)
return True
return False
def is_used(self) -> bool:
if len(self.get_forwards()) == 0:
logging.info(PROXY_LOG_PREFIX + "no forwards: %s", self.region)
@ -194,7 +226,9 @@ class Proxy(ORMMixin):
def get_forwards(self) -> List[Forward]:
forwards: List[Forward] = []
for entry in ProxyForward.search_forward(region=self.region):
for entry in ProxyForward.search_forward(
region=self.region, proxy_id=self.proxy_id
):
if entry.endtime < datetime.datetime.now(tz=datetime.timezone.utc):
entry.delete()
else:
@ -212,7 +246,7 @@ class Proxy(ORMMixin):
proxy_config = ProxyConfig(
url=get_file_sas_url(
Container("proxy-configs"),
"%s/config.json" % self.region,
"%s/%s/config.json" % (self.region, self.proxy_id),
StorageType.config,
read=True,
),
@ -223,6 +257,7 @@ class Proxy(ORMMixin):
),
forwards=forwards,
region=self.region,
proxy_id=self.proxy_id,
instance_telemetry_key=os.environ.get("APPINSIGHTS_INSTRUMENTATIONKEY"),
microsoft_telemetry_key=os.environ.get("ONEFUZZ_TELEMETRY"),
instance_id=get_instance_id(),
@ -230,7 +265,7 @@ class Proxy(ORMMixin):
save_blob(
Container("proxy-configs"),
"%s/config.json" % self.region,
"%s/%s/config.json" % (self.region, self.proxy_id),
proxy_config.json(),
StorageType.config,
)
@ -244,28 +279,22 @@ class Proxy(ORMMixin):
@classmethod
def get_or_create(cls, region: Region) -> Optional["Proxy"]:
proxy = Proxy.get(region)
if proxy is not None:
if proxy.version != __version__:
logging.info(
PROXY_LOG_PREFIX + "mismatch version: proxy:%s service:%s state:%s",
proxy.version,
__version__,
proxy.state,
)
if proxy.state != VmState.stopping:
# If the proxy is out-of-date, delete and re-create it
proxy.state = VmState.stopping
proxy.save()
return None
proxy_list = Proxy.search(query={"region": [region], "outdated": [False]})
for proxy in proxy_list:
if proxy.is_outdated():
proxy.outdated = True
proxy.save()
continue
if proxy.state not in VmState.available():
continue
return proxy
logging.info(PROXY_LOG_PREFIX + "creating proxy: region:%s", region)
proxy = Proxy(region=region)
proxy.save()
send_event(EventProxyCreated(region=region))
send_event(EventProxyCreated(region=region, proxy_id=proxy.proxy_id))
return proxy
def delete(self) -> None:
super().delete()
send_event(EventProxyDeleted(region=self.region))
send_event(EventProxyDeleted(region=self.region, proxy_id=self.proxy_id))