def __init__(self, domain, python_version): self.domain = domain self.python_version = python_version self.project_path = Path(f'~/{domain}').expanduser() self.virtualenv = Virtualenv(self.domain, self.python_version) self.wsgi_file_path = Path( f"/var/www/{domain.replace('.', '_')}_wsgi.py") self.webapp = Webapp(domain)
def test_nuke_option_overrides_all_but_token_check(self, api_token, api_responses, fake_home, virtualenvs_folder): webapp = Webapp(self.domain) (fake_home / self.domain).mkdir() (virtualenvs_folder / self.domain).mkdir() webapp.sanity_checks(nuke=True) # should not raise
def reload(domain_name: str = typer.Option( "your-username.pythonanywhere.com", "-d", "--domain", help="Domain name, eg www.mydomain.com", )): domain_name = ensure_domain(domain_name) webapp = Webapp(domain_name) webapp.reload() typer.echo(snakesay(f"{domain_name} has been reloaded"))
def test_returns_json_from_server_having_parsed_expiry_with_timezone_offset_and_separators( self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/ssl/") api_responses.add( responses.GET, expected_url, status=200, body=json.dumps({ "not_after": "2018-08-24T17:16:23+00:00", "issuer_name": "PythonAnywhere test CA", "subject_name": "www.mycoolsite.com", "subject_alternate_names": ["www.mycoolsite.com", "mycoolsite.com"], }), ) assert Webapp("mydomain.com").get_ssl_info() == { "not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc()), "issuer_name": "PythonAnywhere test CA", "subject_name": "www.mycoolsite.com", "subject_alternate_names": ["www.mycoolsite.com", "mycoolsite.com"], } get = api_responses.calls[0] assert get.request.method == "GET" assert get.request.url == expected_url assert get.request.headers["Authorization"] == f"Token {api_token}"
def test_raises_if_patch_does_not_20x(self, api_responses, api_token): expected_post_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") expected_patch_url = (get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + "mydomain.com/") api_responses.add( responses.POST, expected_post_url, status=201, body=json.dumps({"status": "OK"}), ) api_responses.add( responses.PATCH, expected_patch_url, status=400, json={"message": "an error"}, ) with pytest.raises(Exception) as e: Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=False) assert ( "PATCH to set virtualenv path and source directory via API failed" in str(e.value)) assert "an error" in str(e.value)
def test_does_patch_to_update_virtualenv_path_and_source_directory( self, api_responses, api_token): expected_post_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") expected_patch_url = (get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + "mydomain.com/") api_responses.add( responses.POST, expected_post_url, status=201, body=json.dumps({"status": "OK"}), ) api_responses.add(responses.PATCH, expected_patch_url, status=200) Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=False) patch = api_responses.calls[1] assert patch.request.url == expected_patch_url assert patch.request.body == urlencode({ "virtualenv_path": "/virtualenv/path", "source_directory": "/project/path" }) assert patch.request.headers["Authorization"] == f"Token {api_token}"
def test_does_post_to_create_webapp(self, api_responses, api_token): expected_post_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") expected_patch_url = (get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + "mydomain.com/") api_responses.add( responses.POST, expected_post_url, status=201, body=json.dumps({"status": "OK"}), ) api_responses.add(responses.PATCH, expected_patch_url, status=200) Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=False) post = api_responses.calls[0] assert post.request.url == expected_post_url assert post.request.body == urlencode({ "domain_name": "mydomain.com", "python_version": PYTHON_VERSIONS["3.7"] }) assert post.request.headers["Authorization"] == f"Token {api_token}"
def test_get_list_of_logs(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="files") + "tree/?path=/var/log/") api_responses.add( responses.GET, expected_url, status=200, body=json.dumps([ "/var/log/blah", "/var/log/mydomain.com.access.log", "/var/log/mydomain.com.access.log.1", "/var/log/mydomain.com.access.log.2.gz", "/var/log/mydomain.com.error.log", "/var/log/mydomain.com.error.log.1", "/var/log/mydomain.com.error.log.2.gz", "/var/log/mydomain.com.server.log", "/var/log/mydomain.com.server.log.1", "/var/log/mydomain.com.server.log.2.gz", ]), ) logs = Webapp("mydomain.com").get_log_info() post = api_responses.calls[0] assert post.request.url == expected_url assert post.request.headers["Authorization"] == f"Token {api_token}" assert logs == { "access": [0, 1, 2], "error": [0, 1, 2], "server": [0, 1, 2] }
def test_does_two_posts_to_static_files_endpoint(self, api_token, api_responses): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/static_files/") api_responses.add(responses.POST, expected_url, status=201) api_responses.add(responses.POST, expected_url, status=201) Webapp("mydomain.com").add_default_static_files_mappings( "/project/path") post1 = api_responses.calls[0] assert post1.request.url == expected_url assert post1.request.headers["content-type"] == "application/json" assert post1.request.headers["Authorization"] == f"Token {api_token}" assert json.loads(post1.request.body.decode("utf8")) == { "url": "/static/", "path": "/project/path/static", } post2 = api_responses.calls[1] assert post2.request.url == expected_url assert post2.request.headers["content-type"] == "application/json" assert post2.request.headers["Authorization"] == f"Token {api_token}" assert json.loads(post2.request.body.decode("utf8")) == { "url": "/media/", "path": "/project/path/media", }
def test_raises_if_webapp_already_exists(self, api_token, api_responses): webapp = Webapp(self.domain) api_responses.add( responses.GET, self.expected_url, status=200, body=json.dumps({ "id": 1, "domain_name": self.domain }), ) with pytest.raises(SanityException) as e: webapp.sanity_checks(nuke=False) assert "You already have a webapp for " + self.domain in str(e.value) assert "nuke" in str(e.value)
def main(domain_name, certificate_file, private_key_file, suppress_reload): if not os.path.exists(certificate_file): print(f"Could not find certificate file {certificate_file}") sys.exit(1) with open(certificate_file, "r") as f: certificate = f.read() if not os.path.exists(private_key_file): print(f"Could not find private key file {private_key_file}") sys.exit(1) with open(private_key_file, "r") as f: private_key = f.read() webapp = Webapp(domain_name) webapp.set_ssl(certificate, private_key) if not suppress_reload: webapp.reload() ssl_details = webapp.get_ssl_info() print(snakesay( "That's all set up now :-)\n" "Your new certificate will expire on {expiry:%d %B %Y},\n" "so shortly before then you should renew it\n" "and install the new certificate.".format( expiry=ssl_details["not_after"] ) ))
def test_raises_if_get_does_not_20x(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="files") + "tree/?path=/var/log/") api_responses.add(responses.GET, expected_url, status=404, body="nope") with pytest.raises(Exception) as e: Webapp("mydomain.com").get_log_info() assert "GET log files info via API failed" in str(e.value) assert "nope" in str(e.value)
def test_raises_if_get_does_not_return_200(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/ssl/") api_responses.add(responses.GET, expected_url, status=404, body="nope") with pytest.raises(Exception) as e: Webapp("mydomain.com").get_ssl_info() assert "GET SSL details via API failed, got" in str(e.value) assert "nope" in str(e.value)
def test_delete_old_access_log(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="files") + "path/var/log/mydomain.com.access.log.1/") api_responses.add(responses.DELETE, expected_url, status=200) Webapp("mydomain.com").delete_log(log_type="access", index=1) post = api_responses.calls[0] assert post.request.url == expected_url assert post.request.body is None assert post.request.headers["Authorization"] == f"Token {api_token}"
def test_does_post_to_reload_url(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/reload/") api_responses.add(responses.POST, expected_url, status=200) Webapp("mydomain.com").reload() post = api_responses.calls[0] assert post.request.url == expected_url assert post.request.body is None assert post.request.headers["Authorization"] == f"Token {api_token}"
def test_raises_if_delete_does_not_20x(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="files") + "path/var/log/mydomain.com.access.log/") api_responses.add(responses.DELETE, expected_url, status=404, body="nope") with pytest.raises(Exception) as e: Webapp("mydomain.com").delete_log(log_type="access") assert "DELETE log file via API failed" in str(e.value) assert "nope" in str(e.value)
def test_raises_if_post_does_not_20x(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/ssl/") api_responses.add(responses.POST, expected_url, status=404, body="nope") with pytest.raises(Exception) as e: Webapp("mydomain.com").set_ssl("foo", "bar") assert "POST to set SSL details via API failed" in str(e.value) assert "nope" in str(e.value)
def test_raises_if_post_does_not_20x_that_is_not_a_cname_error( self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/reload/") api_responses.add(responses.POST, expected_url, status=404, body="nope") with pytest.raises(Exception) as e: Webapp("mydomain.com").reload() assert "POST to reload webapp via API failed" in str(e.value) assert "nope" in str(e.value)
def test_raises_if_post_does_not_20x(self, api_responses, api_token): expected_post_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") api_responses.add(responses.POST, expected_post_url, status=500, body="an error") with pytest.raises(Exception) as e: Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=False) assert "POST to create webapp via API failed" in str(e.value) assert "an error" in str(e.value)
def install_ssl( domain_name: str = typer.Argument( ..., help="Domain name, eg www.mydomain.com", ), certificate_file: Path = typer.Argument( ..., exists=True, file_okay=True, readable=True, resolve_path=True, help= "The name of the file containing the combined certificate in PEM format (normally a number of blocks, " 'each one starting "BEGIN CERTIFICATE" and ending "END CERTIFICATE")', ), private_key_file: Path = typer.Argument( ..., exists=True, file_okay=True, readable=True, resolve_path=True, help= "The name of the file containing the private key in PEM format (a file with one block, " 'starting with something like "BEGIN PRIVATE KEY" and ending with something like "END PRIVATE KEY")', ), suppress_reload: bool = typer.Option( False, help= "The website will need to be reloaded in order to activate the new certificate/key combination " "-- this happens by default, use this option to suppress it.", ), ): with open(certificate_file, "r") as f: certificate = f.read() with open(private_key_file, "r") as f: private_key = f.read() webapp = Webapp(domain_name) webapp.set_ssl(certificate, private_key) if not suppress_reload: webapp.reload() ssl_details = webapp.get_ssl_info() typer.echo( snakesay("That's all set up now :-)\n" f"Your new certificate for {domain_name} will expire\n" f"on {ssl_details['not_after'].date().isoformat()},\n" "so shortly before then you should renew it\n" "and install the new certificate."))
def test_does_not_raise_if_post_responds_with_a_cname_error( self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/reload/") api_responses.add( responses.POST, expected_url, status=409, json={ "status": "error", "error": "cname_error" }, ) ## Should not raise Webapp("mydomain.com").reload()
def test_ignores_404_from_delete_call_when_nuking(self, api_responses, api_token): post_url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") webapp_url = (get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + "mydomain.com/") api_responses.add(responses.DELETE, webapp_url, status=404) api_responses.add(responses.POST, post_url, status=201, body=json.dumps({"status": "OK"})) api_responses.add(responses.PATCH, webapp_url, status=200) Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=True)
def test_does_post_to_ssl_url(self, api_responses, api_token): expected_url = (get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") + "mydomain.com/ssl/") api_responses.add(responses.POST, expected_url, status=200) certificate = "certificate data" private_key = "private key data" Webapp("mydomain.com").set_ssl(certificate, private_key) post = api_responses.calls[0] assert post.request.url == expected_url assert json.loads(post.request.body.decode("utf8")) == { "private_key": "private key data", "cert": "certificate data", } assert post.request.headers["Authorization"] == f"Token {api_token}"
def main(domain_name, suppress_reload): homedir = expanduser("~") possible_paths = ( os.path.join(homedir, 'letsencrypt', domain_name), os.path.join(homedir, 'letsencrypt', 'certs', domain_name), ) done = False for path in possible_paths: certificate_file = os.path.join(path, 'fullchain.pem') private_key_file = os.path.join(path, 'privkey.pem') if os.path.exists(certificate_file) and os.path.exists(private_key_file): with open(certificate_file, "r") as f: certificate = f.read() with open(private_key_file, "r") as f: private_key = f.read() webapp = Webapp(domain_name) webapp.set_ssl(certificate, private_key) if not suppress_reload: webapp.reload() ssl_details = webapp.get_ssl_info() print( snakesay( "This method of handling Let's Encrypt certs\n" "**************IS DEPRECATED.**************\n\n" "You can now have a Let's Encrypt certificate managed by PythonAnywhere.\n" "We handle all the details of getting it, installing it,\n" "and managing renewals for you. So you don't need to do\n" "any of the stuff below any more.\n\n" "Anyway, All is set up for now. \n" "Your new certificate will expire on {expiry:%d %B %Y},\n" "so shortly before then you should switch to the new system\n" "(see https://help.pythonanywhere.com/pages/HTTPSSetup/)\n" "".format( expiry=ssl_details["not_after"] ) ) ) done = True break if not done: print("Could not find certificate or key files (looked in {possible_paths})".format( possible_paths=possible_paths )) sys.exit(2)
def test_does_delete_first_for_nuke_call(self, api_responses, api_token): post_url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps") webapp_url = (get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") + "mydomain.com/") api_responses.add(responses.DELETE, webapp_url, status=200) api_responses.add(responses.POST, post_url, status=201, body=json.dumps({"status": "OK"})) api_responses.add(responses.PATCH, webapp_url, status=200) Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=True) delete = api_responses.calls[0] assert delete.request.method == "DELETE" assert delete.request.url == webapp_url assert delete.request.headers["Authorization"] == f"Token {api_token}"
def test_raises_if_post_returns_a_200_with_status_error( self, api_responses, api_token): expected_post_url = get_api_endpoint().format( username=getpass.getuser(), flavor="webapps") api_responses.add( responses.POST, expected_post_url, status=200, body=json.dumps({ "status": "ERROR", "error_type": "bad", "error_message": "bad things happened", }), ) with pytest.raises(Exception) as e: Webapp("mydomain.com").create("3.7", "/virtualenv/path", "/project/path", nuke=False) assert "POST to create webapp via API failed" in str(e.value) assert "bad things happened" in str(e.value)
class Project: def __init__(self, domain, python_version): self.domain = domain self.python_version = python_version self.project_path = Path(f'~/{domain}').expanduser() self.virtualenv = Virtualenv(self.domain, self.python_version) self.wsgi_file_path = Path( f"/var/www/{domain.replace('.', '_')}_wsgi.py") self.webapp = Webapp(domain) def sanity_checks(self, nuke): self.webapp.sanity_checks(nuke=nuke) if nuke: return if self.virtualenv.path.exists(): raise SanityException( "You already have a virtualenv for {domain}.\n\n" "Use the --nuke option if you want to replace it.".format( domain=self.domain)) if self.project_path.exists(): raise SanityException( "You already have a project folder at {project_path}.\n\n" "Use the --nuke option if you want to replace it.".format( project_path=self.project_path)) def create_webapp(self, nuke): self.webapp.create(self.python_version, self.virtualenv.path, self.project_path, nuke=nuke) def add_static_file_mappings(self): self.webapp.add_default_static_files_mappings(self.project_path) def start_bash(self): print( snakesay( 'Starting Bash shell with activated virtualenv in project directory. Press Ctrl+D to exit.' )) unique_id = str(uuid.uuid4()) launch_bash_in_virtualenv(self.virtualenv.path, unique_id, self.project_path)
def main(domain_name): webapp = Webapp(domain_name) webapp.reload() print(snakesay(f"{domain_name} has been reloaded"))
def main(domain, log_type, log_index): webapp = Webapp(ensure_domain(domain)) log_types = ["access", "error", "server"] logs = webapp.get_log_info() if log_type == "all" and log_index == "all": for key in log_types: for log in logs[key]: webapp.delete_log(key, log) elif log_type == "all": for key in log_types: webapp.delete_log(key, int(log_index)) elif log_index == "all": for i in logs[log_type]: webapp.delete_log(log_type, int(i)) else: webapp.delete_log(log_type, int(log_index)) print(snakesay('All Done!'))
def test_does_not_raise_if_no_webapp(self, api_token, api_responses): webapp = Webapp(self.domain) api_responses.add(responses.GET, self.expected_url, status=404) webapp.sanity_checks(nuke=False) # should not raise