diff --git a/.github/workflows/automated-testing.yml b/.github/workflows/automated-testing.yml index a193bdca9..769740d4e 100644 --- a/.github/workflows/automated-testing.yml +++ b/.github/workflows/automated-testing.yml @@ -1,4 +1,4 @@ -name: automated_testing +name: Automated Testing env: AWS_REGION: eu-west-2 permissions: @@ -7,10 +7,10 @@ permissions: on: push: - branches: [ dev, markmac99 ] + branches: [dev, markmac99] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [dev, markmac99] jobs: conttests: runs-on: thelinux @@ -20,7 +20,7 @@ jobs: username: ${{ secrets.DH_LOGIN_MM }} password: ${{ secrets.DH_PATT_MM }} env: - BRANCH: ${{ github.head_ref || github.ref_name }} + BRANCH: ${{ github.head_ref || github.ref_name }} volumes: - testdata:/data steps: @@ -32,7 +32,6 @@ jobs: aws-region: ${{ env.AWS_REGION }} - name: remote tests run: | - chmod +x /tests.sh if [ ! -f /data/admin/cameraLocs.json ] ; then echo "getting test data" curl https://archive.ukmeteors.co.uk/browse/testdata/testdata.tar.gz -o /data/testdata.tar.gz @@ -40,13 +39,24 @@ jobs: else echo "test data available" fi - mkdir -p ~/.aws - cp /data/profile/credentials ~/.aws/ - /tests.sh + aws sts get-caller-identity + pip install --upgrade MeteorTools | grep -v 'already satisfied' + pip install pytest pytest-cov | grep -v 'already satisfied' + [ -d ./ukmda-dataprocessing ] && rm -Rf ./ukmda-dataprocessing + git clone https://github.com/ukmda/ukmda-dataprocessing.git + cd ./ukmda-dataprocessing/ + git checkout $BRANCH + cd ./archive/ukmon_pylib/ + curdir=$(pwd -P) + export DATADIR=/data + echo DATADIR is $DATADIR curdir is $curdir + export PYTHONPATH=/WesternMeteorPyLib:/RMS:${curdir} + pytest -v ./tests --cov=. --cov-report=term-missing + apitests: runs-on: ubuntu-latest steps: - - name: checkout code + - name: checkout code uses: actions/checkout@v3 - name: run api tests run: | diff --git a/.github/workflows/build_usermgmt.yml b/.github/workflows/build_usermgmt.yml deleted file mode 100644 index 5d078517c..000000000 --- a/.github/workflows/build_usermgmt.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: build_usermgmt -env: - AWS_REGION: eu-west-2 -permissions: - id-token: write - contents: read - -on: - push: - branches: [ dev, markmac99, master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] -jobs: - local_tests: - name: local test stage - runs-on: win10 - steps: - - name: "☁️ checkout repository" - uses: actions/checkout@v4 - - name: testing - run: | - echo "hello from test" - conda activate ukmon-admin - dir usermgmt\windows - build: - name: build stage - runs-on: win10 - needs: local_tests - steps: - - name: building - run: | - echo "running build step" - conda activate ukmon-admin - cd usermgmt\windows - pyinstaller ./stationMaint2.py --noconsole --onefile --windowed --icon .\camera.ico - dir dist - package: - name: package app - runs-on: win10 - needs: - - local_tests - - build - steps: - - name: packaging - run: | - echo "packaging the app now" - cd usermgmt\windows\dist - copy ../camera.ico . - ((get-content ..\stationmaint.cfg) -replace "adm_mark","") -replace "~/.ssh/ukmonhelper","" | set-content -path ./stationmaint.cfg - compress-archive -path . -update -destinationpath c:\temp\ukmon_usermgmt.zip - release: - name: release package - runs-on: win10 - needs: - - local_tests - - build - - package - steps: - - name: release - uses: xresloader/upload-to-github-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.MM_PAT }} - with: - file: c:\temp\ukmon_usermgmt.zip - #tags: true - draft: true - overwrite: true - tag_name: 2024.04.02 - default_release_name: User Management Tool - default_release_body_path: usermgmt\windows\release_notes.md diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 86dbe2e6b..77e99e8a1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - - cron: '26 14 * * 6' + - cron: "26 14 * * 6" jobs: analyze: @@ -32,40 +32,40 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript', 'python' ] + language: ["javascript", "python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 531416e0f..e897bccc6 100644 --- a/.gitignore +++ b/.gitignore @@ -593,10 +593,19 @@ archive/samfunctions*/camDetails/pythoncode/camDetails.py archive/samfunctions*/camDetails/pythoncode/awskeys archive/containers/RMS-docker/rms_docker.tar archive/containers/RMS-docker/Dockerfile_orig.txt + +usermgmt/windows/csvkeys +usermgmt/windows/inifs +usermgmt/windows/jsonkeys +usermgmt/windows/keys +usermgmt/windows/sshkeys +usermgmt/windows/stationdetails +usermgmt/windows/users usermgmt/gmailkeys/* usermgmt/server/ukmonfundraising.txt usermgmt/server/ukmoncommittee.txt usermgmt/server/ukmon*.json + **/awskeys.test **/trajsolver_old/* archive/tmp/* @@ -605,3 +614,7 @@ tests/testing/RMS/ tests/testing/WesternMeteorPyLib/ archive/ukmon_pylib/tests/testdata.tar.gz servercopybkp/* +fbcollector/config.ini +usermgmt/windows/stationmaint.ini +usermgmt/windows/README.md +fbCollector/README.html diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index c0662c555..1e99cf94d 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -5,8 +5,7 @@ "includePath": [ "${workspaceFolder}/**", "${vcpkgRoot}/x64-windows/include", - "${vcpkgRoot}/x86-windows/include", - "e:/dev/opencv3/opencv-3.4.11/build/install/include" + "${vcpkgRoot}/x86-windows/include" ], "defines": [ "_DEBUG", diff --git a/archive/analysis/consolidateOutput.sh b/archive/analysis/consolidateOutput.sh index 822038e57..34743434c 100644 --- a/archive/analysis/consolidateOutput.sh +++ b/archive/analysis/consolidateOutput.sh @@ -37,23 +37,29 @@ consdir=${DATADIR}/consolidated/temp mkdir -p ${DATADIR}/single/rawcsvs ls -1 $consdir/*.csv | while read csvf do - bn=$(basename $csvf) - typ=${bn:0:3} - if [ "$typ" != "M20" ] ; then - pref="P" - yr=${bn:7:4} + flen=$(wc -l $csvf | awk '{print $1}') + if [ $flen -lt 2 ] ; then + logger -s -t consolidateOutput "skipping empty file $csvf" + rm $csvf else - pref="M" - yr=${bn:1:4} - fi - mrgfile=${DATADIR}/consolidated/${pref}_${yr}-unified.csv - if [ ! -f $mrgfile ] ; then - cat $csvf >> $mrgfile - else - #echo $bn $mrgfile - sed '1d' $csvf >> $mrgfile + bn=$(basename $csvf) + typ=${bn:0:3} + if [ "$typ" != "M20" ] ; then + pref="P" + yr=${bn:7:4} + else + pref="M" + yr=${bn:1:4} + fi + mrgfile=${DATADIR}/consolidated/${pref}_${yr}-unified.csv + if [ ! -f $mrgfile ] ; then + cat $csvf >> $mrgfile + else + #echo $bn $mrgfile + sed '1d' $csvf >> $mrgfile + fi + mv $csvf ${DATADIR}/single/rawcsvs fi - mv $csvf ${DATADIR}/single/rawcsvs done logger -s -t consolidateOutput "pushing consolidated information back" diff --git a/archive/containers/trajsolver/Dockerfile b/archive/containers/trajsolver/Dockerfile index 276e5a11b..ff8b6ff5b 100644 --- a/archive/containers/trajsolver/Dockerfile +++ b/archive/containers/trajsolver/Dockerfile @@ -1,18 +1,20 @@ # Copyright (C) 2018-2023 Mark McIntyre -FROM continuumio/miniconda3:4.11.0 +#FROM continuumio/miniconda3:4.11.0 +FROM continuumio/miniconda3:24.7.1-0 -RUN conda install -y -c conda-forge "numpy<1.25.0" scipy cython pytz pyqt +#RUN conda install -y -c conda-forge "numpy<1.25.0" scipy cython pytz pyqt +RUN conda install -y -c conda-forge "numpy<2" scipy cython pytz pyqt RUN conda install -y -c conda-forge jplephem ephem RUN conda install -y -c conda-forge basemap basemap-data-hires RUN conda install -y -c conda-forge pandas cartopy -RUN conda install -y matplotlib=3.4.3 +RUN conda install -y matplotlib RUN pip install boto3 RUN apt-get update && apt-get -y upgrade RUN conda install -y -c conda-forge numba COPY WesternMeteorPyLib/ ./WesternMeteorPyLib -ENV PYTHONPATH ./WesternMeteorPyLib -ENV PROJ_LIB ./ +ENV PYTHONPATH=./WesternMeteorPyLib +ENV PROJ_LIB=./ COPY *.py ./ #ENV NUMPY_EXPERIMENTAL_DTYPE_API 1 diff --git a/archive/containers/trajsolver/build.ps1 b/archive/containers/trajsolver/build.ps1 index 8d63c8877..4a4dac048 100644 --- a/archive/containers/trajsolver/build.ps1 +++ b/archive/containers/trajsolver/build.ps1 @@ -17,7 +17,7 @@ write-output "building $imagename in $accid" $yn=read-host -prompt "update WMPL?" if ($yn.tolower() -eq "y") { bash -c "./update_wmpl.sh $env" } -if ($env -eq "test") { copy-item awskeys.test awskeys} +if ($env -eq "test") { move-item awskeys.test awskeys} docker build . -t ${imagename} if (! $?) @@ -26,7 +26,7 @@ if (! $?) exit } set-location $loc -if ($env -eq "test") { Remove-Item awskeys} +if ($env -eq "test") { move-item awskeys awskeys.test } $yn=read-host -prompt "upload to ECR?" if ($yn.tolower() -eq "y") { diff --git a/archive/containers/trajsolver/trajsolver.py b/archive/containers/trajsolver/trajsolver.py index 7155a2359..2d42835f7 100644 --- a/archive/containers/trajsolver/trajsolver.py +++ b/archive/containers/trajsolver/trajsolver.py @@ -36,7 +36,7 @@ def runCorrelator(dir_path, time_beg, time_end): trajectory_constraints.geometric_uncert = not uncerttime # Clock for measuring script time - t1 = datetime.datetime.utcnow() + t1 = datetime.datetime.now(datetime.timezone.utc) # If auto run is enabled, compute the time range to use event_time_range = None @@ -115,7 +115,7 @@ def runCorrelator(dir_path, time_beg, time_end): tc = TrajectoryCorrelator(dh, trajectory_constraints, velpart, data_in_j2000=True, distribute=distribute, enableOSM=True) tc.run(event_time_range=event_time_range) - print("Total run time: {:s}".format(str(datetime.datetime.utcnow() - t1))) + print("Total run time: {:s}".format(str(datetime.datetime.now(datetime.timezone.utc) - t1))) return @@ -136,9 +136,7 @@ def getSourceAndTargets(): webbucket = webpth[:webpth.find('/')] webpth = webpth[webpth.find('/')+1:] - oldoutb = os.getenv('OLDOUTB', default=None) - oldwebb = os.getenv('OLDWEBB', default=None) - return srcbucket, srcpth, outbucket, outpth, webbucket, webpth, oldwebb, oldoutb + return srcbucket, srcpth, outbucket, outpth, webbucket, webpth # extra args for setting the MIME type when uploading to S3. @@ -168,10 +166,31 @@ def getExtraArgs(fname): return extraargs +def checkIfFileNeeded(filename): + + # update this if there's an additional file created by WMPL that we want + # to use on the website + # NB THIS ALSO HAS TO BE CHANGED in maintenance.recreateOrbitPagesif more files needed + if 'orbit_top.png' in filename or 'orbit_side.png' in filename or 'ground_track.png' in filename: + return True + if 'velocities.png' in filename or 'lengths.png' in filename or 'lags_all.png' in filename: + return True + if 'abs_mag.png' in filename or 'abs_mag_ht.png' in filename or 'report.txt' in filename: + return True + if 'all_angular_residuals.png' in filename or 'all_spatial_total_residuals_height.png' in filename: + return True + if 'trajectory.pickle' in filename: + return True + return False + + #push solutions to the website, and push the pickle and report to shared bucket -def pushToWebsite(s3, localfldr, webbucket, webfldr, outbucket, outpth, oldwebb=None, oldoutb=None): +def pushToWebsite(s3, localfldr, webbucket, webfldr, outbucket, outpth): for root, _, files in os.walk(localfldr): for fil in files: + if not checkIfFileNeeded(fil): + print(f'skipping {fil}') + continue fullname = os.path.join(localfldr, root, fil) fname = os.path.split(fil)[1] orbname = os.path.split(root)[1] @@ -181,26 +200,10 @@ def pushToWebsite(s3, localfldr, webbucket, webfldr, outbucket, outpth, oldwebb= targkey = f'{webfldr}/{yr}/orbits/{ym}/{ymd}/{orbname}/{fname}' print(f'uploading {fname} to s3://{webbucket}/{targkey}') s3.meta.client.upload_file(fullname, webbucket, targkey, ExtraArgs = getExtraArgs(fname)) - if oldwebb: - print(f' and uploading to {oldwebb}') - try: - s3.meta.client.upload_file(fullname, oldwebb, targkey, ExtraArgs = getExtraArgs(fname)) - except: - print('unable to push to old website') - else: - print('not pushing to old website') if 'report' in fname or 'pickle' in fname: targkey = f'{outpth}/trajectories/{yr}/{ym}/{ymd}/{orbname}/{fname}' print(f'uploading {fname} to s3://{outbucket}/{targkey}') s3.meta.client.upload_file(fullname, outbucket, targkey, ExtraArgs = getExtraArgs(fname)) - if oldoutb: - print(f' and uploading to {oldoutb}') - try: - s3.meta.client.upload_file(fullname, oldoutb, targkey, ExtraArgs = getExtraArgs(fname)) - except: - print('unable to push to old share') - else: - print('not pushing to old share') return @@ -238,7 +241,7 @@ def startup(srcfldr, startdt, enddt, isTest=False): os.makedirs(canddir, exist_ok = True) s3 = getS3Client() - srcbucket, srcpth, outbucket, outpth, webbucket, webpth, oldwebb, oldoutb = getSourceAndTargets() + srcbucket, srcpth, outbucket, outpth, webbucket, webpth = getSourceAndTargets() if isTest is True: outpth = os.path.join(outpth, 'test') @@ -263,7 +266,7 @@ def startup(srcfldr, startdt, enddt, isTest=False): # reacquire tokens just in case the 1hour time limit on chained roles is exceeded s3 = getS3Client() - pushToWebsite(s3, trajfldr, webbucket, webpth, outbucket, outpth, oldwebb=oldwebb, oldoutb=oldoutb) + pushToWebsite(s3, trajfldr, webbucket, webpth, outbucket, outpth) fname = f'{srcfldr}.json' jsonfile = os.path.join(localfldr, 'processed_trajectories.json') diff --git a/archive/cronjobs/deleteDuplicateOrbit.sh b/archive/cronjobs/deleteDuplicateOrbit.sh new file mode 100644 index 000000000..03d129c4d --- /dev/null +++ b/archive/cronjobs/deleteDuplicateOrbit.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +here="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +source $here/../config.ini >/dev/null 2>&1 +conda activate $HOME/miniconda3/envs/${WMPL_ENV} + +chkrunning=.delrunning +mkdir -p $DATADIR/manualuploads +cd $DATADIR/manualuploads +if [[ -f ./$chkrunning || -f ./running ]] ; then + echo "update already running" + exit +fi +touch ./$chkrunning +aws s3 sync $UKMONSHAREDBUCKET/fireballs/uploads . --exclude "*" --include "*.delete" --exclude "*.done" + +newfiles=$(ls -1 *.delete 2> /dev/null) +if [ "$newfiles" != "" ] ; then + for fil in $newfiles ; do + orbname=$(cat $fil) + echo $(date +%Y-%m-%dT%H:%M:%SZ) removing $orbname + $SRC/utils/deleteOrbit.sh $orbname + aws s3 mv $UKMONSHAREDBUCKET/fireballs/uploads/$fil $UKMONSHAREDBUCKET/fireballs/uploads/processed/$fil.done + rm $fil + done +fi +rm $DATADIR/manualuploads/$chkrunning +find $SRC/logs -name "deleteDuplicateOrbit*" -mtime +10 -exec rm -f {} \; diff --git a/archive/cronjobs/mergeNewOrbit.sh b/archive/cronjobs/mergeNewOrbit.sh index cfdf264b6..0738450f0 100644 --- a/archive/cronjobs/mergeNewOrbit.sh +++ b/archive/cronjobs/mergeNewOrbit.sh @@ -6,6 +6,11 @@ conda activate $HOME/miniconda3/envs/${WMPL_ENV} mkdir -p $DATADIR/manualuploads cd $DATADIR/manualuploads +if [ -f ./running ] ; then + echo "already running" + exit +fi +touch .running aws s3 sync $UKMONSHAREDBUCKET/fireballs/uploads . --exclude "*" --include "*.zip" --exclude "*.done" newfiles=$(ls -1 *.zip 2> /dev/null) @@ -57,4 +62,5 @@ EOD #else # echo nothing to process fi -find $SRC/logs -name "mergeNewOrbit*" -mtime +10 -exec rm -f {} \; \ No newline at end of file +rm $DATADIR/manualuploads/.running +find $SRC/logs -name "mergeNewOrbit*" -mtime +10 -exec rm -f {} \; diff --git a/archive/cronjobs/nightlyJob.sh b/archive/cronjobs/nightlyJob.sh index e96dcfddf..7f184a5fd 100644 --- a/archive/cronjobs/nightlyJob.sh +++ b/archive/cronjobs/nightlyJob.sh @@ -29,6 +29,7 @@ mkdir -p $DATADIR/browse/{annual,monthly,daily,showers} log2cw $NJLOGGRP $NJLOGSTREAM "updating the camera location/dir/fov database" nightlyJob python -c "from reports.CameraDetails import updateCamLocDirFovDB; updateCamLocDirFovDB();" aws s3 cp $DATADIR/admin/cameraLocs.json $UKMONSHAREDBUCKET/admin/ --profile ukmonshared --quiet +aws s3 cp $DATADIR/admin/cameraLocs.json $WEBSITEBUCKET/browse/ --profile ukmonshared --quiet aws s3 sync $UKMONSHAREDBUCKET/admin/ $DATADIR/admin --profile ukmonshared --quiet # create the CSV file of camera info and the html versions for search functions on the website diff --git a/archive/deployment/analysis.yml b/archive/deployment/analysis.yml index faeb27f7b..8e217f41d 100644 --- a/archive/deployment/analysis.yml +++ b/archive/deployment/analysis.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/archive/deployment/config.yml b/archive/deployment/config.yml index 2fa3d319e..b9823e58a 100644 --- a/archive/deployment/config.yml +++ b/archive/deployment/config.yml @@ -1,6 +1,8 @@ - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-datapocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: dev-vars.yml diff --git a/archive/deployment/cronjobs.yml b/archive/deployment/cronjobs.yml index 1232a22ec..6a80c1b5e 100644 --- a/archive/deployment/cronjobs.yml +++ b/archive/deployment/cronjobs.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/Dev/meteorhunting/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml @@ -19,6 +21,7 @@ - {src: '{{srcdir}}/cronjobs/nightlyJob.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } - {src: '{{srcdir}}/cronjobs/captureBright.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } - {src: '{{srcdir}}/cronjobs/getImoWSfile.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } - - {src: '{{srcdir}}//cronjobs/mergeNewOrbit.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } - - {src: '{{srcdir}}//cronjobs/gatherMonthlyVideos.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } + - {src: '{{srcdir}}/cronjobs/mergeNewOrbit.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } + - {src: '{{srcdir}}/cronjobs/deleteDuplicateOrbit.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } + - {src: '{{srcdir}}/cronjobs/gatherMonthlyVideos.sh', dest: '{{destdir}}/cronjobs', mode: '754', backup: no } - {src: '{{srcdir}}/cronjobs/README.md', dest: '{{destdir}}/cronjobs', mode: '644', backup: no } diff --git a/archive/deployment/database.yml b/archive/deployment/database.yml index 1baceec67..1b75b1f5f 100644 --- a/archive/deployment/database.yml +++ b/archive/deployment/database.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/usermgmt/deploy-server.yml b/archive/deployment/deploy-usermgmt.yml similarity index 51% rename from usermgmt/deploy-server.yml rename to archive/deployment/deploy-usermgmt.yml index ff6ebb521..8429780d3 100644 --- a/usermgmt/deploy-server.yml +++ b/archive/deployment/deploy-usermgmt.yml @@ -1,22 +1,18 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/usermgmt/server + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/meteorhunting/ukmda-dataprocessing/archive/usermgmt" + destdir: /home/ec2-user/keymgmt tasks: - - name: import dev variables - include_vars: ./dev-vars.yml - tags: dev - - name: import prod variables - include_vars: ./prod-vars.yml - tags: prod - name: Ensures {{destdir}} exists file: path={{destdir}} state=directory - tags: [dev,prod] - name: Copy files - tags: [dev,prod] copy: src={{ item.src }} dest={{ item.dest }} mode={{ item.mode }} with_items: - {src: '{{srcdir}}/addSftpUser.sh', dest: '{{destdir}}/', mode: '744', backup: yes, directory_mode: no } - {src: '{{srcdir}}/updateAwsKey.sh', dest: '{{destdir}}/', mode: '744', backup: yes, directory_mode: no } - - {src: '{{srcdir}}/ukmon.ini', dest: '{{destdir}}/', mode: '644', backup: yes, directory_mode: no } + - {src: '{{srcdir}}/addAdminUser.sh', dest: '{{destdir}}/', mode: '744', backup: yes, directory_mode: no } + - {src: '{{srcdir}}/addFBApiKey.sh', dest: '{{destdir}}/', mode: '744', backup: yes, directory_mode: no } diff --git a/archive/deployment/pylib.yml b/archive/deployment/pylib.yml index 90ae4e01d..46a4afc71 100644 --- a/archive/deployment/pylib.yml +++ b/archive/deployment/pylib.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml @@ -35,6 +37,7 @@ - {src: '{{srcdir}}/ukmon_pylib/converters/__init__.py', dest: '{{destdir}}/ukmon_pylib/converters', mode: '644', backup: no } - {src: '{{srcdir}}/ukmon_pylib/converters/README.md', dest: '{{destdir}}/ukmon_pylib/converters', mode: '644', backup: no } + - {src: '{{srcdir}}/ukmon_pylib/maintenance/compressOldArchiveData.py', dest: '{{destdir}}/ukmon_pylib/maintenance', mode: '644', backup: no } - {src: '{{srcdir}}/ukmon_pylib/maintenance/cleanDdbTables.py', dest: '{{destdir}}/ukmon_pylib/maintenance', mode: '644', backup: no } - {src: '{{srcdir}}/ukmon_pylib/maintenance/dataMaintenance.py', dest: '{{destdir}}/ukmon_pylib/maintenance', mode: '644', backup: no } - {src: '{{srcdir}}/ukmon_pylib/maintenance/deDuplicate.py', dest: '{{destdir}}/ukmon_pylib/maintenance', mode: '644', backup: no } diff --git a/archive/deployment/server_setup.yml b/archive/deployment/server_setup.yml index 4682ee3b5..f621dc278 100644 --- a/archive/deployment/server_setup.yml +++ b/archive/deployment/server_setup.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/archive/deployment/shwrinfo.yml b/archive/deployment/shwrinfo.yml index 0c5dbc78c..17a6110ee 100644 --- a/archive/deployment/shwrinfo.yml +++ b/archive/deployment/shwrinfo.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/archive/deployment/utils.yml b/archive/deployment/utils.yml index e6a47f723..f122d7cb5 100644 --- a/archive/deployment/utils.yml +++ b/archive/deployment/utils.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/archive/deployment/website.yml b/archive/deployment/website.yml index 427008f1d..1ee42364e 100644 --- a/archive/deployment/website.yml +++ b/archive/deployment/website.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml diff --git a/archive/deployment/website_static.yml b/archive/deployment/website_static.yml index edb791d7b..cbe146679 100644 --- a/archive/deployment/website_static.yml +++ b/archive/deployment/website_static.yml @@ -1,7 +1,9 @@ --- - hosts: "{{host | default ('ukmonhelper2')}}" + vars_files: + - /mnt/c/Users/{{ lookup('env','USER' )}}/apikeys/mqvariables.enc vars: - srcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/archive + srcdir: "/mnt/c/Users/{{ lookup('env','USER' )}}/OneDrive/dev/ukmda-dataprocessing/archive" tasks: - name: import dev variables include_vars: ./dev-vars.yml @@ -18,7 +20,7 @@ with_items: - {src: '{{srcdir}}/static_content/browse/', dest: '{{destdir}}/static_content/browse', mode: '754', backup: no, directory_mode: yes } - {src: '{{srcdir}}/static_content/css/', dest: '{{destdir}}/static_content/css', mode: '754', backup: no, directory_mode: yes } - - {src: '{{srcdir}}/static_content/data/', dest: '{{destdir}}/static_content/data', mode: '754', backup: no, directory_mode: yes } + #- {src: '{{srcdir}}/static_content/data/', dest: '{{destdir}}/static_content/data', mode: '754', backup: no, directory_mode: yes } - {src: '{{srcdir}}/static_content/docs/', dest: '{{destdir}}/static_content/docs', mode: '754', backup: no, directory_mode: yes } - {src: '{{srcdir}}/static_content/fonts/', dest: '{{destdir}}/static_content/fonts', mode: '754', backup: no, directory_mode: yes } - {src: '{{srcdir}}/static_content/img/', dest: '{{destdir}}/static_content/img', mode: '754', backup: no, directory_mode: yes } diff --git a/archive/samfunctions/getExtraFilesV2/pythoncode/Dockerfile b/archive/samfunctions/getExtraFilesV2/pythoncode/Dockerfile index 801df9512..26f9034fa 100644 --- a/archive/samfunctions/getExtraFilesV2/pythoncode/Dockerfile +++ b/archive/samfunctions/getExtraFilesV2/pythoncode/Dockerfile @@ -1,16 +1,17 @@ # Copyright (C) 2018-2023 Mark McIntyre -FROM public.ecr.aws/lambda/python:3.9 + +FROM public.ecr.aws/lambda/python:3.13 COPY requirements.txt ./ RUN python -m pip install -r requirements.txt -t . RUN python -m pip install numba watchdog basemap-data-hires -t . RUN python -m pip install cartopy -t . -RUN yum update -y -ENV LD_LIBRARY_PATH ./ +#RUN yum update -y +ENV LD_LIBRARY_PATH=./ COPY WesternMeteorPyLib/ ./WesternMeteorPyLib #COPY *.npy ./WesternMeteorPyLib/wmpl/share/ COPY *.py ./ -ENV PYTHONPATH ./WesternMeteorPyLib -ENV PROJ_LIB ./ +ENV PYTHONPATH=./WesternMeteorPyLib +ENV PROJ_LIB=./ CMD ["getExtraFiles.lambda_handler"] diff --git a/archive/samfunctions/getExtraFilesV2/pythoncode/createOrbitPageIndex.py b/archive/samfunctions/getExtraFilesV2/pythoncode/createOrbitPageIndex.py index 35f5497ac..ab4e24184 100644 --- a/archive/samfunctions/getExtraFilesV2/pythoncode/createOrbitPageIndex.py +++ b/archive/samfunctions/getExtraFilesV2/pythoncode/createOrbitPageIndex.py @@ -31,9 +31,9 @@ def createOrbitPageIndex(fldr, websitebucket, s3): lis = sumf.readlines() idxf.writelines(lis) - zipf = orbitname + '.zip' - if os.path.isfile(os.path.join(fldr, zipf)): - idxf.write(f"Click here to download a zip of the raw and processed data.\n") + #zipf = orbitname + '.zip' + #if os.path.isfile(os.path.join(fldr, zipf)): + # idxf.write(f"Click here to download a zip of the raw and processed data.\n") idxf.write("\n") idxf.write("

Detailed report below graphs

\n") idxf.write("

Click on an image to see a larger view

\n") diff --git a/archive/samfunctions/getExtraFilesV2/pythoncode/getExtraFiles.py b/archive/samfunctions/getExtraFilesV2/pythoncode/getExtraFiles.py index 6d4827a72..8c71bd7b3 100644 --- a/archive/samfunctions/getExtraFilesV2/pythoncode/getExtraFiles.py +++ b/archive/samfunctions/getExtraFilesV2/pythoncode/getExtraFiles.py @@ -5,7 +5,6 @@ import sys import shutil import json -from zipfile import ZipFile, ZIP_DEFLATED from datetime import datetime, timedelta from boto3.dynamodb.conditions import Key @@ -81,12 +80,15 @@ def generateExtraFiles(key, archbucket, websitebucket, ddb, s3): try: js = json.loads(obs.comment) ffname = js['ff_name'] - print(ffname) + if ffname[0] == '[': + ffname = ffname[2:-2].split(',') + if isinstance(ffname, list): + ffname = ffname[0] # case when filename is nonstandard if 'FF_' not in ffname and 'FR_' not in ffname: ffname = 'FF_' + ffname ffname = ffname.replace('FR_', 'FF_').replace('.bin', '.fits') - if '.bin' not in ffname and '.fits' not in ffname: + if '.bin' not in ffname and '.fits' not in ffname and '.jpg' not in ffname: ffname = ffname + '.fits' gotff = True except Exception: @@ -98,13 +100,15 @@ def generateExtraFiles(key, archbucket, websitebucket, ddb, s3): else: pass if gotff is True: + print(ffname) spls = ffname.split('_') id = spls[1] dtstr = spls[2] tmstr = spls[3] - jpgname=f'img/single/{dtstr[:4]}/{dtstr[:6]}/{ffname}'.replace('fits','jpg') - mp4name=f'img/mp4/{dtstr[:4]}/{dtstr[:6]}/{ffname}'.replace('fits','mp4') + jpgname=f'img/single/{dtstr[:4]}/{dtstr[:6]}/{ffname}'.replace('.fits','.jpg') + mp4name=f'img/mp4/{dtstr[:4]}/{dtstr[:6]}/{ffname}'.replace('.fits','.mp4').replace('.jpg','.mp4') print('about to get a list of available jpgs') + print(jpgname) res = s3.meta.client.list_objects_v2(Bucket=websitebucket,Prefix=jpgname) if res['KeyCount'] > 0: jpgf.write(f'{jpgname}\n') @@ -157,7 +161,6 @@ def generateExtraFiles(key, archbucket, websitebucket, ddb, s3): print('no extrampgs.html') pass - # pushFilesBack creates the zipfile so we need to do this first print('pushing files back') pushFilesBack(outdir, archbucket, websitebucket, fuloutdir, s3) print('updating the index page') @@ -239,9 +242,8 @@ def findOtherFiles(evtdt, archbucket, websitebucket, outdir, fldr, s3): def pushFilesBack(outdir, archbucket, websitebucket, fldr, s3): - # get filelist before creating the zipfile! - flist = os.listdir(outdir) + flist = os.listdir(outdir) _, pth =os.path.split(outdir) yr = pth[:4] ym = pth[:6] @@ -251,13 +253,9 @@ def pushFilesBack(outdir, archbucket, websitebucket, fldr, s3): else: webpth = f'reports/{yr}/orbits/{ym}/{pth}/' - zipfname = os.path.join(outdir, pth +'.zip') - zipf = ZipFile(zipfname, 'w', compression=ZIP_DEFLATED, compresslevel=9) - for f in flist: - #print(f) + print(f) locfname = os.path.join(outdir, f) - zipf.write(locfname) # some files need to be pushed to the website, some to the archive bucket if '3dtrack' in f or '2dtrack' in f: key = os.path.join(webpth, f) @@ -277,11 +275,6 @@ def pushFilesBack(outdir, archbucket, websitebucket, fldr, s3): extraargs = getExtraArgs(locfname) s3.meta.client.upload_file(locfname, archbucket, key, ExtraArgs=extraargs) - zipf.close() - # now we push the zipfile - key = os.path.join(webpth, pth + '.zip') - extraargs = getExtraArgs(zipfname) - s3.meta.client.upload_file(zipfname, websitebucket, key, ExtraArgs=extraargs) return diff --git a/archive/samfunctions/getExtraFilesV2/template.yaml b/archive/samfunctions/getExtraFilesV2/template.yaml index a2d87dfe4..54dd99243 100644 --- a/archive/samfunctions/getExtraFilesV2/template.yaml +++ b/archive/samfunctions/getExtraFilesV2/template.yaml @@ -38,4 +38,4 @@ Resources: Metadata: Dockerfile: Dockerfile DockerContext: ./pythoncode - DockerTag: python3.9-v1 + DockerTag: python3.13-v1 diff --git a/archive/samfunctions/getExtraFilesV2/tests/localTest.ps1 b/archive/samfunctions/getExtraFilesV2/tests/localTest.ps1 index 154cf55c2..6c33151be 100644 --- a/archive/samfunctions/getExtraFilesV2/tests/localTest.ps1 +++ b/archive/samfunctions/getExtraFilesV2/tests/localTest.ps1 @@ -1,4 +1,4 @@ # Copyright (C) 2018-2023 Mark McIntyre -sam build +#sam build sam local invoke --profile ukmda_admin -e tests\testEvent.json #sam local invoke --profile ukmda_admin -e tests\testEvent2.json \ No newline at end of file diff --git a/archive/samfunctions/getExtraFilesV2/update_wmpl.sh b/archive/samfunctions/getExtraFilesV2/update_wmpl.sh index e06a917b2..57d3fb6fa 100644 --- a/archive/samfunctions/getExtraFilesV2/update_wmpl.sh +++ b/archive/samfunctions/getExtraFilesV2/update_wmpl.sh @@ -14,3 +14,4 @@ rm -Rf ./WesternMeteorPyLib/wmpl/MetSim rm -Rf ./WesternMeteorPyLib/wmpl/CAMO rm -f ./WesternMeteorPyLib/wmpl/Utils/DynamicMassFit.py rm -f ./WesternMeteorPyLib/wmpl/Utils/PlotMap_OSM.py +cp ./wmpl__init__.py_fixed.py ./WesternMeteorPyLib/wmpl/__init__.py diff --git a/archive/samfunctions/getExtraFilesV2/wmpl__init__.py_fixed.py b/archive/samfunctions/getExtraFilesV2/wmpl__init__.py_fixed.py new file mode 100644 index 000000000..dc34c35dc --- /dev/null +++ b/archive/samfunctions/getExtraFilesV2/wmpl__init__.py_fixed.py @@ -0,0 +1,45 @@ +# This is a fixed-up version of WesternMeteorPyLib/wmpl/__init__.py which is python 3.12+ compatible + +""" Import all submodules. """ + +import importlib +import pkgutil +import sys + +# Excluded packages +exclude = ["MetSim.ML", "GUI"] + +__all__ = [] +for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): + + # Skip the config module + if 'Config' in module_name: + continue + + # Skip Supracenter + if 'Supracenter' in module_name: + continue + + + ### Skip exluded packages ### + skip_package = False + for exclude_test in exclude: + if exclude_test in module_name: + skip_package = True + break + + if skip_package: + continue + + ### ### + + + __all__.append(module_name) + if sys.version_info.major > 2 and sys.version_info.minor > 12: + module_spec = loader.find_spec(module_name) + if module_spec is not None: + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + else: + module = loader.find_module(module_name).load_module(module_name) + exec('%s = module' % module_name) diff --git a/archive/samfunctions/getLiveImages/getLiveImages.py b/archive/samfunctions/getLiveImages/getLiveImages.py index 16202cd1a..164ecc605 100644 --- a/archive/samfunctions/getLiveImages/getLiveImages.py +++ b/archive/samfunctions/getLiveImages/getLiveImages.py @@ -53,7 +53,7 @@ def getTrueImgs(dtstr, dtstr2, statid, ddb=None): if d1.hour < 13: capnight=(d1 + datetime.timedelta(days=-1)).strftime('%Y%m%d') else: - capnight = dtstr[:8] + capnight = d1.strftime('%Y%m%d') lv=Decimal(f'{d1.timestamp():.0f}') hv=Decimal(f'{d2.timestamp():.0f}') resp = table.query(KeyConditionExpression=Key('CaptureNight').eq(int(capnight)) & Key('Timestamp').between(lv, hv), diff --git a/archive/samfunctions/matchDataApi/localTest.ps1 b/archive/samfunctions/matchDataApi/localTest.ps1 new file mode 100644 index 000000000..c71174dee --- /dev/null +++ b/archive/samfunctions/matchDataApi/localTest.ps1 @@ -0,0 +1,3 @@ +# Copyright (C) 2018-2023 Mark McIntyre +# sam build --profile ukmonshared --region eu-west-1 +sam local invoke --profile ukmonshared -e $args[0] --region eu-west-1 diff --git a/archive/samfunctions/matchDataApi/matchDataApi.py b/archive/samfunctions/matchDataApi/matchDataApi.py new file mode 100644 index 000000000..b7c2023ef --- /dev/null +++ b/archive/samfunctions/matchDataApi/matchDataApi.py @@ -0,0 +1,169 @@ +# Copyright (C) 2018-2023 Mark McIntyre +# +# python code behind the MatchData API +# + +import os +import boto3 +import json +import pymysql.cursors + + +def getSqlLoginDetails(): + # retrieve password and host from SSM. This allows me to manage them from Terraform + ssm = boto3.client('ssm', region_name='eu-west-1') + res = ssm.get_parameter(Name='prod_dbpw', WithDecryption=True) + password = res['Parameter']['Value'] + res = ssm.get_parameter(Name='prod_dbhost') + host = res['Parameter']['Value'] + # should really do these too but they won't change often if at all + user = 'batch' + db = 'ukmon' + return host, user, password, db + + +def getStationData(statid, dtstr): + host, user, passwd, db = getSqlLoginDetails() + connection = pymysql.connect(host=host, user=user, password=passwd, db=db, cursorclass=pymysql.cursors.DictCursor) + try: + with connection.cursor() as cursor: + if statid is None: + sql = f"SELECT s.orbname from matches s where s._localtime like '_{dtstr}%'" + else: + sql = f"SELECT s.orbname from matches s where s._localtime like '_{dtstr}%' and s.stations like '%{statid}%'" + cursor.execute(sql) + result = cursor.fetchall() + finally: + connection.close() + res='' + for event in result: + res = res + json.dumps(event) + '\n' + return res + + +def getSummaryData(dtstr): + host, user, passwd, db = getSqlLoginDetails() + connection = pymysql.connect(host=host, user=user, password=passwd, db=db, cursorclass=pymysql.cursors.DictCursor) + fieldlist = '_localtime,_mjd,_sol,_ID1,_amag,_ra_o,_dc_o,_ra_t,_dc_t,_elng,_elat,_vo,_vi,_vg,_vs,_a,_q,_e,_p,_peri,_node,_incl,'\ + '_stream,_mag,_dur,_lng1,_lat1,_H1,_lng2,_lat2,_H2,_LD21,_az1r,_ev1r,_Nts,_Nos,_leap,_tme,_dt,'\ + 'dtstamp,orbname,iau,shwrname as name,mass,pi,Q,true_anom,EA,MA,Tj,T,last_peri,jacchia1,Jacchia2,numstats,stations' + expr = f"SELECT {fieldlist} from matches s where s._localtime like '_{dtstr}%'" + result=[] + try: + with connection.cursor() as cursor: + cursor.execute(expr) + result = cursor.fetchall() + finally: + connection.close() + if len(result) > 0: + res = json.dumps(result) + else: + res=json.dumps({'result': 'no data'}) + res = res.replace('}, {','},\n{') + return res + + +def getDetailData(orbname): + host, user, passwd, db = getSqlLoginDetails() + connection = pymysql.connect(host=host, user=user, password=passwd, db=db, cursorclass=pymysql.cursors.DictCursor) + expr = f"SELECT * from matches s where s.orbname like '{orbname}%'" + try: + with connection.cursor() as cursor: + cursor.execute(expr) + result = cursor.fetchall() + finally: + connection.close() + if len(result) > 0: + res = json.dumps(result[0]) + else: + res=json.dumps({'result': 'no data'}) + res = res.replace('}, {','},\n{') + return res + + +def lambda_handler(event, context): + webbuck = os.getenv('WEBBUCKET', default='ukmda-website') + #print('received event', json.dumps(event)) + qs = event['queryStringParameters'] + if qs is None: + return { + 'statusCode': 200, + 'body': 'usage: detections?reqtyp=xxx&reqval=yyyy[&points=1]' + } + reqtyp = qs['reqtyp'] + points = False + if 'points' in qs: + points = True + + if reqtyp == 'station': + statid = qs['statid'] + dtstr = qs['reqval'] + print(f'station data requested for {statid} on {dtstr}') + res = getStationData(statid, dtstr) + elif reqtyp == 'summary': + dtstr = qs['reqval'] + print(f'summary data requested for {dtstr}') + res = getSummaryData(dtstr) + elif reqtyp == 'matches': + dtstr = qs['reqval'] + print(f'match data requested for {dtstr}') + res = getStationData(None, dtstr) + elif reqtyp == 'detail': + orbname = qs['reqval'] + print(f'detail requested for {orbname}') + res = getDetailData(orbname) + if points: + print(f'points requested for {orbname}') + if 'imgstr' in res: + url = json.loads(res)['imgstr'] + url = url.replace('ground_track.png','report.txt') + reppth = url.split('//', 1)[1].split('/',1)[1] + _, repname = os.path.split(reppth) + try: + tmpfname = f'/tmp/{repname}' + s3 = boto3.client('s3') + s3.download_file(webbuck, reppth, tmpfname) + flis = open(tmpfname, 'r').readlines() + os.remove(tmpfname) + res = fileToJsonString(flis) + except Exception: + print('report file unavailable') + res = '{"points": "unavailable"}' + else: + res = '{"points": "unavailable"}' + + else: + res = '{"invalid request type - must be one of \'matches\', \'details\', \'station\',\'summary\'"}' + + print(res) + return { + 'statusCode': 200, + 'body': res + } + + +def fileToJsonString(flis): + hdr = ['No','statid','ign','t','jd','m1','m2','az','alt','azl','altl','rao', + 'deco','ral','decl','X','Y','Z','lat','lon','H','range','length','svd', + 'lag','vel','pvel', 'hres','vres','ares','vmag','amag'] + ptsarray='[' + gotpts = False + for fli in flis: + if 'Points' in fli: + gotpts = True + continue + elif '------' in fli or ' No' in fli: + continue + elif gotpts is True and (len(fli) < 2 or 'Notes' in fli): + gotpts=False + break + elif gotpts is True: + spls = fli.split(',') + thisrow = '{' + for h, s in zip(hdr, spls): + thisrow = thisrow + f'"{h}": "{s.strip()}",' + thisrow = thisrow[:-1] + '},' + ptsarray = ptsarray + thisrow + ptsarray = ptsarray[:-1] + ']' + ptsarray = '{' + f'"points": {ptsarray}' + '}' + return ptsarray diff --git a/archive/samfunctions/matchDataApi/requirements.txt b/archive/samfunctions/matchDataApi/requirements.txt new file mode 100644 index 000000000..917ec643c --- /dev/null +++ b/archive/samfunctions/matchDataApi/requirements.txt @@ -0,0 +1,6 @@ +# +# requirements for matchDataApi +# Copyright (C) 2025 Mark McIntyre +# +# mysql-connector-python +pymysql diff --git a/archive/samfunctions/matchDataApi/samconfig.toml b/archive/samfunctions/matchDataApi/samconfig.toml new file mode 100644 index 000000000..074b92a09 --- /dev/null +++ b/archive/samfunctions/matchDataApi/samconfig.toml @@ -0,0 +1,12 @@ +# Copyright (C) 2018-2023 Mark McIntyre +version = 0.1 +[default] +[default.deploy] +[default.deploy.parameters] +stack_name = "matchDataApi" +s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1cy2k1achj9nv" +s3_prefix = "matchDataApi" +region = "eu-west-1" +capabilities = "CAPABILITY_IAM" +image_repositories = [] +profile = "ukmda_admin" diff --git a/archive/samfunctions/matchDataApi/template.yml b/archive/samfunctions/matchDataApi/template.yml new file mode 100644 index 000000000..d07529b3a --- /dev/null +++ b/archive/samfunctions/matchDataApi/template.yml @@ -0,0 +1,38 @@ +# SAM build file for searchArchive +# Copyright (C) 2018-2023 Mark McIntyre +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Resources: + matchDataApi: + Type: AWS::Serverless::Function + Properties: + Handler: matchDataApi.lambda_handler + Runtime: python3.11 + FunctionName: matchDataApi + Description: API backend that fetches match data + Timeout: 120 + Policies: [AmazonS3ReadOnlyAccess, AmazonSSMReadOnlyAccess ] + Environment: + Variables: + ARCHBUCKET: ukmda-shared + MPLCONFIGDIR: /tmp/mpl + Events: + HttpGet: + Type: Api + Properties: + Path: '/' + Method: get + RequestParameters: + - method.request.querystring.reqtyp: + Required: true + - method.request.querystring.reqval: + Required: true + - method.request.querystring.points: + Required: false + - method.request.querystring.statid: + Required: false + Tags: + billingtag: "ukmda" + typetag: "api" + Metadata: + BuildMethod: python3.11 diff --git a/archive/samfunctions/matchDataApi/testDetail.json b/archive/samfunctions/matchDataApi/testDetail.json new file mode 100644 index 000000000..ee4ab4132 --- /dev/null +++ b/archive/samfunctions/matchDataApi/testDetail.json @@ -0,0 +1,7 @@ +{ + "httpMethod": "GET", + "queryStringParameters": { + "reqtyp": "detail", + "reqval": "20251008_235928.502_UK" + } +} \ No newline at end of file diff --git a/archive/samfunctions/matchDataApi/testMatches.json b/archive/samfunctions/matchDataApi/testMatches.json new file mode 100644 index 000000000..67a03d178 --- /dev/null +++ b/archive/samfunctions/matchDataApi/testMatches.json @@ -0,0 +1,7 @@ +{ + "httpMethod": "GET", + "queryStringParameters": { + "reqtyp": "matches", + "reqval": "20251008" + } +} \ No newline at end of file diff --git a/archive/samfunctions/matchDataApi/testPoints.json b/archive/samfunctions/matchDataApi/testPoints.json new file mode 100644 index 000000000..afcac2367 --- /dev/null +++ b/archive/samfunctions/matchDataApi/testPoints.json @@ -0,0 +1,8 @@ +{ + "httpMethod": "GET", + "queryStringParameters": { + "reqtyp": "detail", + "reqval": "20251008_235928.502_UK", + "points": 1 + } +} \ No newline at end of file diff --git a/archive/samfunctions/matchDataApi/testStation.json b/archive/samfunctions/matchDataApi/testStation.json new file mode 100644 index 000000000..22e338fa0 --- /dev/null +++ b/archive/samfunctions/matchDataApi/testStation.json @@ -0,0 +1,8 @@ +{ + "httpMethod": "GET", + "queryStringParameters": { + "reqtyp": "station", + "statid": "UK0006", + "reqval": "20251008" + } +} \ No newline at end of file diff --git a/archive/samfunctions/matchDataApi/testSummary.json b/archive/samfunctions/matchDataApi/testSummary.json new file mode 100644 index 000000000..785013bcb --- /dev/null +++ b/archive/samfunctions/matchDataApi/testSummary.json @@ -0,0 +1,7 @@ +{ + "httpMethod": "GET", + "queryStringParameters": { + "reqtyp": "summary", + "reqval": "20251008" + } +} \ No newline at end of file diff --git a/archive/samfunctions/updateDnsRecords/lambda_function.py b/archive/samfunctions/updateDnsRecords/lambda_function.py new file mode 100644 index 000000000..255058f61 --- /dev/null +++ b/archive/samfunctions/updateDnsRecords/lambda_function.py @@ -0,0 +1,130 @@ +import os +import boto3 +import logging + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +""" Default values """ +DNS_TAG_KEY = 'Route53FQDN' +DNS_RECORD_TYPE = 'CNAME' +DNS_RECORD_TTL = 60 + + +def update_route53_record(region, instance_id, dns_tag_key, dns_record_type, dns_record_ttl): + """ + update function + """ + dns_record_type = dns_record_type.upper() + + """ Get EC2 Description """ + ec2 = boto3.client('ec2', region_name=region) + ec2_desc_res = ec2.describe_instances(InstanceIds=[instance_id]) + if ('Reservations' not in ec2_desc_res) or (len(ec2_desc_res['Reservations']) < 1) or (len(ec2_desc_res['Reservations'][0]['Instances']) < 1): + logger.warning('Error hetting EC2 instance information failure. Instance Id: ' + instance_id) + return + if (ec2_desc_res['Reservations'][0]['Instances'][0]['State']['Name'] != 'running'): + logger.warning('EC2 state is not running. Current state: ' + ec2_desc_res['Reservations'][0]['Instances'][0]['State']['Name']) + return + ec2_public_dns = ec2_desc_res['Reservations'][0]['Instances'][0]['PublicDnsName'] + ec2_public_ip = ec2_desc_res['Reservations'][0]['Instances'][0]['PublicIpAddress'] + + """ Get EC2 Tags """ + ec2_tags_res = ec2.describe_tags(Filters=[ + {'Name': 'resource-id', 'Values': [instance_id]}, + {'Name': 'key', 'Values': [dns_tag_key]}]) + + if ('Tags' not in ec2_tags_res) or (len(ec2_tags_res['Tags']) < 1): + logger.warning('EC2 instance has no DNS tag. Instance Id: ' + instance_id) + return + + dns_record_name = ec2_tags_res['Tags'][0]['Value'] + if (dns_record_name[-1] == '.'): + dns_record_name = dns_record_name[:-1] + + ec2_tags_res = ec2.describe_tags(Filters=[ + {'Name': 'resource-id', 'Values': [instance_id]}, + {'Name': 'key', 'Values': ['DNSRecordType']}]) + if ('Tags' not in ec2_tags_res) or (len(ec2_tags_res['Tags']) < 1): + logger.warning('EC2 instance has no Record Type. Instance Id: ' + instance_id) + else: + dns_record_type = ec2_tags_res['Tags'][0]['Value'] + + """ Check DNS name """ + dns_record_names = dns_record_name.split('.') + if (len(dns_record_names) < 3): + logger.warning('The FQDN must over 3 levels at least. Instance Id: ' + instance_id + ', ' + dns_tag_key + ': ' + dns_record_name) + return + """ Get all hosted zones from Route53 """ + route53 = boto3.client('route53', region_name=region) + zones_res = route53.list_hosted_zones() + all_zones = zones_res['HostedZones'] + if (zones_res['IsTruncated']): + while zones_res and (not zones_res['IsTruncated']): + zones_res = route53.list_hosted_zones(Marker=zones_res['NextMarker']) + all_zones = all_zones + zones_res['HostedZones'] + + """ Find Route53 zone """ + zone_id = None + for i in range(1, len(dns_record_names)-1): + cur_zone_name = '.'.join(dns_record_names[i:len(dns_record_names)+1]) + '.' + cur_zone_matches = [zone for zone in all_zones if (zone['Name']==cur_zone_name)] + if (len(cur_zone_matches) > 0): + zone_id = cur_zone_matches[0]['Id'] + break + if (not zone_id): + print('Route53 zone is not found. DNS name: ' + dns_record_name) + return + + """ Update Route53 record record """ + route53.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': dns_record_name + '.', + 'Type': dns_record_type, + 'ResourceRecords': [{'Value': (ec2_public_dns if (dns_record_type=='CNAME') else ec2_public_ip)}], + 'TTL': dns_record_ttl + } + } + ] + } + ) + logger.info('The Route53 record updated successfully. ' + dns_record_name + ': [' + dns_record_type + '] ' + (ec2_public_dns if (dns_record_type=='CNAME') else ec2_public_ip)) + return + + +def lambda_handler(event, context): + """ + main function + """ + region = os.environ.get("region") + instance_id = os.environ.get("instance_id") + dns_tag_key = os.environ.get("dns_tag_key") if os.environ.get("dns_tag_key") else DNS_TAG_KEY + dns_record_type = os.environ.get("dns_record_type") if os.environ.get("dns_record_type") else DNS_RECORD_TYPE + dns_record_ttl = int(os.environ.get("dns_record_ttl")) if os.environ.get("dns_record_ttl") else DNS_RECORD_TTL + + if (('region' in event) and event['region']): + region = str(event['region']) + if (not region): + print('Parameter "region" is not defined.') + return + + if (('detail' in event) and ('instance-id' in event['detail']) and event['detail']['instance-id']): + instance_id = str(event['detail']['instance-id']) + if (not instance_id): + print('Parameter "instance-id" is not defined.') + return + + if (('state' in event['detail']) and event['detail']['state'] and (event['detail']['state'] != 'running')): + logger.warning('EC2 state is not running. Current state: ' + event['detail']['state']) + return + + try: + update_route53_record(region, instance_id, dns_tag_key, dns_record_type, dns_record_ttl) + except Exception as error: + logger.exception(error) + return diff --git a/archive/samfunctions/updateDnsRecords/samconfig.toml b/archive/samfunctions/updateDnsRecords/samconfig.toml new file mode 100644 index 000000000..2703b7f39 --- /dev/null +++ b/archive/samfunctions/updateDnsRecords/samconfig.toml @@ -0,0 +1,14 @@ +# Copyright (C) 2018-2023 Mark McIntyre +version = 0.1 +[default] +[default.deploy] +[default.deploy.parameters] +stack_name = "updateDnsRecords" +s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1ulna2hoktme4" +s3_prefix = "updateDnsRecords" +region = "eu-west-2" +confirm_changeset = true +capabilities = "CAPABILITY_IAM" +disable_rollback = true +image_repositories = [] +profile = "ukmda_admin" \ No newline at end of file diff --git a/archive/samfunctions/updateDnsRecords/template.yaml b/archive/samfunctions/updateDnsRecords/template.yaml new file mode 100644 index 000000000..f23b62b13 --- /dev/null +++ b/archive/samfunctions/updateDnsRecords/template.yaml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS Lambda function for updating Route53 record when public ip of EC2 instance. + +Resources: + updateDnsRecords: + Type: AWS::Serverless::Function + Properties: + FunctionName: updateDnsRecords + MemorySize: 128 + Handler: lambda_function.lambda_handler + Timeout: 10 + Policies: + - Statement: + - Action: + - ec2:DescribeInstances + - ec2:DescribeTags + - route53:ListHostedZones + - route53:ChangeResourceRecordSets + Resource: '*' + Effect: Allow + Runtime: python3.11 + Events: + EC2StateChangeEvent: + Type: CloudWatchEvent + Properties: + Pattern: + source: + - aws.ec2 + detail: + state: + - running + Description: AWS Lambda function for updating Route53 record when public ip of EC2 instance. \ No newline at end of file diff --git a/archive/static_content/browse/annual/index.html b/archive/static_content/browse/annual/index.html index b71378c10..e775af6d4 100644 --- a/archive/static_content/browse/annual/index.html +++ b/archive/static_content/browse/annual/index.html @@ -40,6 +40,7 @@
+

The data here are released under the CC BY 4.0 license, so if you are using the data whether for scientific or other purposes, your must reference this web site in your work. diff --git a/archive/static_content/browse/daily/index.html b/archive/static_content/browse/daily/index.html index e03a52b93..0d6cc5807 100644 --- a/archive/static_content/browse/daily/index.html +++ b/archive/static_content/browse/daily/index.html @@ -63,6 +63,7 @@

+

Daily Simplified Report of Detections

This page provides a list of all detections in the last 72 hours from the camera network, primarily for the purposes of data exchange with diff --git a/archive/static_content/browse/index.html b/archive/static_content/browse/index.html index ceff96e73..23f8d2e43 100644 --- a/archive/static_content/browse/index.html +++ b/archive/static_content/browse/index.html @@ -41,6 +41,7 @@

+

Browse the Archive.

Follow the links below to access downloadable datasets from our archive.

You can also use our REST APIs to retrieve some data programmatically. If you're interested in diff --git a/archive/static_content/browse/monthly/index.html b/archive/static_content/browse/monthly/index.html index cd1f5f72a..e0f8cf24b 100644 --- a/archive/static_content/browse/monthly/index.html +++ b/archive/static_content/browse/monthly/index.html @@ -41,6 +41,7 @@

+

The data here are released under the CC BY 4.0 license, so if you are using the data whether for scientific or other purposes, your must reference this web site in your work. diff --git a/archive/static_content/css/dragontail.css b/archive/static_content/css/dragontail.css index 35ca1de7a..ddefb159e 100644 --- a/archive/static_content/css/dragontail.css +++ b/archive/static_content/css/dragontail.css @@ -141,7 +141,7 @@ body { .sticky + .content { padding-top: 60px; } - + .sidebar .sidebar-nav.navbar-collapse { padding-right: 0; padding-left: 0; @@ -524,6 +524,12 @@ li > a:only-child:after { content: ''; } font-size: 15px; } +@media screen and (max-width: 1100px) { + .sidenav { + display: none !important; + } +} + .table { /*color: whitesmoke;*/ background-color: white; diff --git a/archive/static_content/docs/Code_of_Conduct.html b/archive/static_content/docs/Code_of_Conduct.html index 8d6024a51..20c9502d7 100644 --- a/archive/static_content/docs/Code_of_Conduct.html +++ b/archive/static_content/docs/Code_of_Conduct.html @@ -43,6 +43,7 @@

+

Contributor Covenant Code of Conduct

Our Pledge

diff --git a/archive/static_content/docs/Committee.html b/archive/static_content/docs/Committee.html index 68bdaccf3..9ce4053af 100644 --- a/archive/static_content/docs/Committee.html +++ b/archive/static_content/docs/Committee.html @@ -53,13 +53,14 @@
+

The Committee

The Society is managed by a committee consisting of the following offices:

- + diff --git a/archive/static_content/docs/index.html b/archive/static_content/docs/index.html index c6003069b..4e32063c5 100644 --- a/archive/static_content/docs/index.html +++ b/archive/static_content/docs/index.html @@ -43,6 +43,7 @@
+

Documents.

UK Meteors is an unincorporated society following the standard model for Astronomy clubs.

Our main website is here and diff --git a/archive/static_content/docs/otherdocs.html b/archive/static_content/docs/otherdocs.html index 8b9ad7833..1979c9508 100644 --- a/archive/static_content/docs/otherdocs.html +++ b/archive/static_content/docs/otherdocs.html @@ -43,6 +43,7 @@

+

Other Documents.

A repository of the Society's public documents.

diff --git a/archive/static_content/fundraising.html b/archive/static_content/fundraising.html index f7fe7e06a..c000f9455 100644 --- a/archive/static_content/fundraising.html +++ b/archive/static_content/fundraising.html @@ -40,6 +40,7 @@

+

Fundraising

If you'd still like to help with data analysis costs, you can donate directly via our bank account. This has the advantage that we are not charged administration fees by diff --git a/archive/static_content/img/ukmon-logo-jpg-crp.jpg b/archive/static_content/img/ukmon-logo-jpg-crp.jpg new file mode 100644 index 000000000..fd9c5fd59 Binary files /dev/null and b/archive/static_content/img/ukmon-logo-jpg-crp.jpg differ diff --git a/archive/static_content/img/ukmon-logo-sm.png b/archive/static_content/img/ukmon-logo-sm.png new file mode 100644 index 000000000..5cbf17f0c Binary files /dev/null and b/archive/static_content/img/ukmon-logo-sm.png differ diff --git a/archive/static_content/privacy_data.html b/archive/static_content/privacy_data.html index b56fe3613..c7e0f55f9 100644 --- a/archive/static_content/privacy_data.html +++ b/archive/static_content/privacy_data.html @@ -39,8 +39,9 @@
+

Privacy and Data Protection Policy

-

Last reviewed 2023-08-15

+

Last reviewed 2025-06-13

This page explains how we handle any personal and sensitive data that we collect.


Camera ID, name and geographic location of each camera. @@ -48,7 +49,7 @@ We collect the Camera ID, general location name and precise geographic coordinates of each camera for the following purposes: -
  • For Scientific data analysis such as statistical reporting of showers, orbit and trajectory +
  • For Scientific data analysis such as statistical reporting of showers, and for orbit and trajectory calculations of meteors detected by the camera. The results of this analysis are made available on this website.
  • @@ -66,7 +67,8 @@

    For example, we might collect Camera ID UK1234, location name Sunthorp*, geographic coordinates 53.62501N 0.08567E, altitude -20m. If a paper is published which makes use of data from this - camera, the author might mention the camera ID and approximate location in the paper. + camera, the author might mention the camera ID and location name in the paper. The precise + geographic coordinates will not be shared without your permission.

    This information will be retained indefinitely even if you cease to contribute. @@ -92,9 +94,9 @@


    Postal Address

    - If you have asked us to build a camera for you, the camera builder will also ask for + If you have asked us to build a camera for you, the camera builder will ask for your full Postal Address so that the items can be posted. We do not retain this information - anywhere on our systems. + anywhere on our online systems and the camera builder will only share it with your permission.


    Publications @@ -128,7 +130,7 @@ this data with other meteor networks if they wish to do so.
  • By agreeing to contribute, you are agreeing that Level 1 and Level 2 data may be used - by the network coordinators to manually produce other products such as Level 3 data, and that + by the network and its coordinators to manually or automatically produce other products such as Level 3 data, and that publication of such results may include any Level 2 data that was used, where this is necessary to explain or validate the Level 3 data.
    For example, when Level 2 data is combined to calculate an orbit or trajectory (Level 3 data), then the relevant Level 2 data may be made diff --git a/archive/static_content/search/index.html b/archive/static_content/search/index.html index c4d6d25a9..5fa067bec 100644 --- a/archive/static_content/search/index.html +++ b/archive/static_content/search/index.html @@ -75,6 +75,7 @@

    Search the Archive.

    +

    This page allows you to search the archive for specific events in the matched and single-station detections.
    Time should be entered as local time, so in the UK summer you should enter the time in BST. diff --git a/archive/static_content/templates/navbar.html b/archive/static_content/templates/navbar.html index fdd38bf7b..061b0d599 100644 --- a/archive/static_content/templates/navbar.html +++ b/archive/static_content/templates/navbar.html @@ -2,9 +2,9 @@

    diff --git a/archive/terraform/mjmm/crossacct-role.tf b/archive/terraform/mjmm/crossacct-role.tf index 90b8f135f..fabcd4ecd 100644 --- a/archive/terraform/mjmm/crossacct-role.tf +++ b/archive/terraform/mjmm/crossacct-role.tf @@ -24,8 +24,12 @@ data "aws_iam_policy_document" "terraformpoldoc" { resource "aws_iam_role" "terraformrole" { name = "TerraformRole" assume_role_policy = data.aws_iam_policy_document.terraformpoldoc.json - managed_policy_arns = [data.aws_iam_policy.terraformpol.arn] tags = { billingtag = "ukmon" } } + +resource "aws_iam_role_policy_attachment" "tfrole_pol_attachment" { + role = aws_iam_role.terraformrole.name + policy_arn = data.aws_iam_policy.terraformpol.arn +} diff --git a/archive/terraform/mjmm/ec2.tf b/archive/terraform/mjmm/ec2.tf index 2e396ea28..e5979d6e9 100644 --- a/archive/terraform/mjmm/ec2.tf +++ b/archive/terraform/mjmm/ec2.tf @@ -2,10 +2,10 @@ # built in aws_infra project as shared resources data "aws_security_group" "ec2publicsg" { - name = "ec2PublicSG" + name = "ec2PublicSG" } data "aws_key_pair" "marks_key" { - key_name = "markskey" + key_name = "markskey" } @@ -15,6 +15,7 @@ resource "aws_instance" "ukmonhelper_g" { iam_instance_profile = aws_iam_instance_profile.S3FullAccess.name key_name = data.aws_key_pair.marks_key.key_name security_groups = [data.aws_security_group.ec2publicsg.name] + force_destroy = false root_block_device { tags = { @@ -24,16 +25,18 @@ resource "aws_instance" "ukmonhelper_g" { } volume_size = 50 volume_type = "gp3" - throughput = 125 - iops = 3000 - encrypted = true - kms_key_id = aws_kms_key.container_key.arn + throughput = 125 + iops = 3000 + encrypted = true + kms_key_id = aws_kms_key.container_key.arn } tags = { - "Name" = "UKMonHelper2" - "billingtag" = "ukmon" - "project" = "UKMonHelper2" + "Name" = "UKMonHelper2" + "billingtag" = "ukmon" + "project" = "UKMonHelper2" + "Route53FQDN" = "ukmonhelper.markmcintyreastro.co.uk" + "DNSRecordType" = "A" } } diff --git a/archive/terraform/mjmm/iam.tf b/archive/terraform/mjmm/iam.tf index 597b828ca..766e2d668 100644 --- a/archive/terraform/mjmm/iam.tf +++ b/archive/terraform/mjmm/iam.tf @@ -70,6 +70,14 @@ resource "aws_iam_policy" "pol4s3fullaccess" { "*", ] }, + { + Action = [ + "iam:GetCredentialReport", + "iam:GenerateCredentialReport" + ] + Effect = "Allow" + Resource = [ "*" ] + }, { Sid = "PassRolePermission" Effect = "Allow" diff --git a/archive/terraform/mjmm/s3.tf b/archive/terraform/mjmm/s3.tf index a23697722..740ba4cc6 100644 --- a/archive/terraform/mjmm/s3.tf +++ b/archive/terraform/mjmm/s3.tf @@ -56,6 +56,7 @@ data "aws_iam_policy_document" "websiteacesspolicy" { ######################################################################## resource "aws_s3_bucket" "mjmm-ukmon-shared" { bucket = "mjmm-ukmon-shared" + timeouts {} tags = { "billingtag" = "ukmon" } @@ -79,7 +80,8 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "mjmm_ukmon_shared } resource "aws_s3_bucket_lifecycle_configuration" "shared_lifecycle_rule" { - bucket = aws_s3_bucket.mjmm-ukmon-shared.id + bucket = aws_s3_bucket.mjmm-ukmon-shared.id + transition_default_minimum_object_size = "varies_by_storage_class" rule { id = "MoveToArchive" status = "Enabled" @@ -87,18 +89,16 @@ resource "aws_s3_bucket_lifecycle_configuration" "shared_lifecycle_rule" { days = 30 storage_class = "STANDARD_IA" } + filter {} } rule { id = "PurgeOldVersions" status = "Enabled" expiration { - days = 0 - expired_object_delete_marker = true - } - - filter { + days = 0 } + filter {} noncurrent_version_expiration { newer_noncurrent_versions = "1" @@ -140,7 +140,9 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "mjmm_ukmon_live_e } resource "aws_s3_bucket_lifecycle_configuration" "live_lifecycle_rule" { - bucket = aws_s3_bucket.mjmm-ukmon-live.id + bucket = aws_s3_bucket.mjmm-ukmon-live.id + transition_default_minimum_object_size = "varies_by_storage_class" + rule { id = "MoveToArchive" status = "Enabled" @@ -148,18 +150,17 @@ resource "aws_s3_bucket_lifecycle_configuration" "live_lifecycle_rule" { days = 30 storage_class = "STANDARD_IA" } + filter {} } rule { id = "PurgeOldVersions" status = "Enabled" expiration { - days = 0 - expired_object_delete_marker = true + days = 0 } - filter { - } + filter {} noncurrent_version_expiration { newer_noncurrent_versions = "1" diff --git a/archive/terraform/mjmm/secgrp.tf b/archive/terraform/mjmm/secgrp.tf index 290136180..859d8cff6 100644 --- a/archive/terraform/mjmm/secgrp.tf +++ b/archive/terraform/mjmm/secgrp.tf @@ -14,6 +14,7 @@ resource "aws_security_group" "default" { description = "default VPC security group" vpc_id = data.aws_vpc.main_vpc.id revoke_rules_on_delete = false + timeouts {} ingress = [ { description = "http in" @@ -60,9 +61,9 @@ resource "aws_security_group" "default" { self = false } ] -egress = [ + egress = [ { - cidr_blocks = ["0.0.0.0/0",] + cidr_blocks = ["0.0.0.0/0", ] description = "" from_port = 0 ipv6_cidr_blocks = [] @@ -120,13 +121,13 @@ resource "aws_security_group" "launch-wizard-4" { cidr_blocks = [] description = "IPv6 SSH for Admin" from_port = 22 - ipv6_cidr_blocks = ["::/0",] + ipv6_cidr_blocks = ["::/0", ] prefix_list_ids = [] protocol = "tcp" security_groups = [] self = false to_port = 22 - }, + }, ] egress = [ { diff --git a/archive/terraform/ukmda/apigw-domain.tf b/archive/terraform/ukmda/apigw-domain.tf index 3cb7995ea..1baacdf31 100644 --- a/archive/terraform/ukmda/apigw-domain.tf +++ b/archive/terraform/ukmda/apigw-domain.tf @@ -40,6 +40,11 @@ data "aws_api_gateway_rest_api" "matchpickleapi" { provider = aws.eu-west-1-prov } +data "aws_api_gateway_rest_api" "matchdataapi" { + name = "matchDataApi" + provider = aws.eu-west-1-prov +} + data "aws_api_gateway_rest_api" "liveimagesapi" { name = "getLiveImages" provider = aws.eu-west-1-prov @@ -71,6 +76,14 @@ resource "aws_api_gateway_base_path_mapping" "camdetapi" { provider = aws.eu-west-1-prov } +resource "aws_api_gateway_base_path_mapping" "matchdataapi" { + api_id = data.aws_api_gateway_rest_api.matchdataapi.id + stage_name = "Prod" + domain_name = aws_api_gateway_domain_name.apigwdomain.domain_name + base_path = "matches" + provider = aws.eu-west-1-prov +} + resource "aws_api_gateway_base_path_mapping" "pickleapi" { api_id = data.aws_api_gateway_rest_api.matchpickleapi.id stage_name = "Prod" diff --git a/archive/terraform/ukmda/cloudwatch.tf b/archive/terraform/ukmda/cloudwatch.tf index 10e038755..61ab690a0 100644 --- a/archive/terraform/ukmda/cloudwatch.tf +++ b/archive/terraform/ukmda/cloudwatch.tf @@ -3,33 +3,6 @@ # # Copyright (C) 2018-2023 Mark McIntyre -resource "aws_cloudwatch_metric_alarm" "calcServerDiskSpace" { - alarm_name = "CalcServer diskspace" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = "1" - metric_name = "disk_used_percent" - namespace = "CWAgent" - period = "300" - statistic = "Average" - threshold = "90" - alarm_description = "CalcServer diskspace has gone over 90%" - insufficient_data_actions = [] - datapoints_to_alarm = 1 - alarm_actions = [aws_sns_topic.ukmdaalerts.arn, ] - dimensions = { - "ImageId" = aws_instance.calc_server.ami - "InstanceId" = aws_instance.calc_server.id - "InstanceType" = aws_instance.calc_server.instance_type - "device" = "nvme0n1p1" - "fstype" = "xfs" - "path" = "/" - } - tags = { - "billingtag" = "ukmda" - } - actions_enabled = true -} - resource "aws_cloudwatch_metric_alarm" "calcServerIdle" { alarm_name = "CalcServer idle shutdown" comparison_operator = "LessThanOrEqualToThreshold" diff --git a/archive/terraform/ukmda/ec2.tf b/archive/terraform/ukmda/ec2.tf index 8bcb950e3..29c5fd71c 100644 --- a/archive/terraform/ukmda/ec2.tf +++ b/archive/terraform/ukmda/ec2.tf @@ -1,13 +1,16 @@ # Copyright (C) 2018-2023 Mark McIntyre resource "aws_instance" "calc_server" { - ami = "ami-0df2d8f6def0bd716" - instance_type = "c6g.4xlarge" - iam_instance_profile = aws_iam_instance_profile.calcserverrole.name - key_name = aws_key_pair.marks_key.key_name + ami = "ami-0df2d8f6def0bd716" + instance_type = "c8g.2xlarge" + iam_instance_profile = aws_iam_instance_profile.calcserverrole.name + key_name = aws_key_pair.marks_key.key_name + force_destroy = false tags = { - "Name" = "Calcengine" - "billingtag" = "ukmda" + "Name" = "Calcengine" + "billingtag" = "ukmda" + "Route53FQDN" = "calcengine.ukmeteors.co.uk" + "DNSRecordType" = "A" } root_block_device { tags = { @@ -16,9 +19,12 @@ resource "aws_instance" "calc_server" { } volume_size = 100 } - network_interface { + primary_network_interface { network_interface_id = aws_network_interface.calcserver_if.id - device_index = 0 + } + + metadata_options { + http_tokens = "required" } } @@ -40,13 +46,15 @@ resource "aws_network_interface" "calcserver_if" { ################################################ resource "aws_instance" "ubuntu_calc_server" { - ami = "ami-0bdf149a42243bde8" - instance_type = "c6g.4xlarge" - iam_instance_profile = aws_iam_instance_profile.calcserverrole.name - key_name = aws_key_pair.marks_key.key_name + ami = "ami-0bdf149a42243bde8" + instance_type = "c6g.4xlarge" + iam_instance_profile = aws_iam_instance_profile.calcserverrole.name + key_name = aws_key_pair.marks_key.key_name tags = { - "Name" = "Calcengine2" - "billingtag" = "ukmda" + "Name" = "calcengine_ub" + "billingtag" = "ukmda" + "Route53FQDN" = "calcengine_ub.ukmeteors.co.uk" + "DNSRecordType" = "A" } root_block_device { tags = { @@ -55,9 +63,11 @@ resource "aws_instance" "ubuntu_calc_server" { } volume_size = 100 } - network_interface { + primary_network_interface { network_interface_id = aws_network_interface.ubuntu_calcserver_if.id - device_index = 0 + } + metadata_options { + http_tokens = "required" } } @@ -81,13 +91,15 @@ resource "aws_network_interface" "ubuntu_calcserver_if" { resource "aws_instance" "admin_server" { - ami = "ami-0c1ce90bf42d9802b" - instance_type = "t2.micro" - iam_instance_profile = aws_iam_instance_profile.S3FullAccess.name - key_name = aws_key_pair.marks_key.key_name + ami = "ami-0c1ce90bf42d9802b" + instance_type = "t2.micro" + iam_instance_profile = aws_iam_instance_profile.S3FullAccess.name + key_name = aws_key_pair.marks_key.key_name tags = { - "Name" = "AdminServer" - "billingtag" = "ukmda" + "Name" = "AdminServer" + "billingtag" = "ukmda" + "Route53FQDN" = "adminserver.ukmeteors.co.uk" + "DNSRecordType" = "A" } root_block_device { tags = { @@ -96,14 +108,8 @@ resource "aws_instance" "admin_server" { } volume_size = 8 } -} - -# elastic IP attached to the calc_server -resource "aws_eip" "adminserver_eip" { - instance = aws_instance.calc_server.id - domain = "vpc" - tags = { - billingtag = "ukmda" + metadata_options { + http_tokens = "required" } -} +} diff --git a/archive/terraform/ukmda/files/matchApiJson/ukmdaMatchApi.json b/archive/terraform/ukmda/files/matchApiJson/ukmdaMatchApi.json deleted file mode 100644 index 4c7473d64..000000000 --- a/archive/terraform/ukmda/files/matchApiJson/ukmdaMatchApi.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "openapi" : "3.0.1", - "info" : { - "title" : "ukmonMatchApi", - "description" : "return info about daily matches", - "version" : "2021-02-15T19:23:10Z" - }, - "servers" : [ { - "url" : "https://oaa3lqdkvf.execute-api.eu-west-1.amazonaws.com/{basePath}", - "variables" : { - "basePath" : { - "default" : "/prod" - } - } - } ], - "paths" : { - "/" : { - "get" : { - "parameters" : [ { - "name" : "reqval", - "in" : "query", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "name" : "reqtyp", - "in" : "query", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "name" : "points", - "in" : "query", - "required" : false, - "schema" : { - "type" : "string" - } - }, { - "name" : "statid", - "in" : "query", - "required" : false, - "schema" : { - "type" : "string" - } - } - ], - "responses" : { - "200" : { - "description" : "200 response", - "headers" : { - "Access-Control-Allow-Origin" : { - "schema" : { - "type" : "string" - } - } - }, - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/Empty" - } - } - } - } - }, - "x-amazon-apigateway-request-validator" : "Validate body, query string parameters, and headers", - "x-amazon-apigateway-integration" : { - "httpMethod" : "POST", - "uri" : "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-2:${thisacct}:function:matchDataApiHandler/invocations", - "responses" : { - "default" : { - "statusCode" : "200", - "responseParameters" : { - "method.response.header.Access-Control-Allow-Origin" : "'*'" - } - } - }, - "passthroughBehavior" : "when_no_match", - "contentHandling" : "CONVERT_TO_TEXT", - "type" : "aws_proxy" - } - }, - "options" : { - "responses" : { - "200" : { - "description" : "200 response", - "headers" : { - "Access-Control-Allow-Origin" : { - "schema" : { - "type" : "string" - } - }, - "Access-Control-Allow-Methods" : { - "schema" : { - "type" : "string" - } - }, - "Access-Control-Allow-Headers" : { - "schema" : { - "type" : "string" - } - } - }, - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/Empty" - } - } - } - } - }, - "x-amazon-apigateway-integration" : { - "responses" : { - "default" : { - "statusCode" : "200", - "responseParameters" : { - "method.response.header.Access-Control-Allow-Methods" : "'GET,OPTIONS'", - "method.response.header.Access-Control-Allow-Headers" : "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", - "method.response.header.Access-Control-Allow-Origin" : "'*'" - } - } - }, - "requestTemplates" : { - "application/json" : "{\"statusCode\": 200}" - }, - "passthroughBehavior" : "when_no_match", - "type" : "mock" - } - } - } - }, - "components" : { - "schemas" : { - "Empty" : { - "title" : "Empty Schema", - "type" : "object" - } - } - }, - "x-amazon-apigateway-request-validators" : { - "Validate body, query string parameters, and headers" : { - "validateRequestParameters" : true, - "validateRequestBody" : true - } - } -} \ No newline at end of file diff --git a/archive/terraform/ukmda/files/matchDataApi/matchDataApiHandler.py b/archive/terraform/ukmda/files/matchDataApi/matchDataApiHandler.py deleted file mode 100644 index 6b1b2e966..000000000 --- a/archive/terraform/ukmda/files/matchDataApi/matchDataApiHandler.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (C) 2018-2023 Mark McIntyre -import datetime -import os -import boto3 -import json - - -def lambda_handler(event, context): - target = os.getenv('SRCHBUCKET', default='ukmda-shared') - webbuck = os.getenv('WEBBUCKET', default='ukmda-website') - #print('received event', json.dumps(event)) - qs = event['queryStringParameters'] - if qs is None: - return { - 'statusCode': 200, - 'body': 'usage: detections?reqtyp=xxx&reqval=yyyy[&points=1]' - } - reqtyp = qs['reqtyp'] - reqval = qs['reqval'] - points = False - if 'points' in qs: - points = True - - if reqtyp not in ['matches','detail','station','summary']: - res = '{"invalid request type - must be one of \'matches\', \'details\', \'station\',\'summary\'"}' - else: - print(f'{reqtyp} {reqval} {points}') - if reqtyp == 'summary': - d1 = datetime.datetime.strptime(reqval, '%Y%m%d') - idxfile = 'matches/matched/matches-full-{:04d}.csv'.format(d1.year) - res = '{"no matches"}' - fieldlist = '_localtime,_mjd,_sol,_ID1,_amag,_ra_o,_dc_o,_ra_t,_dc_t,_elng,_elat,_vo,_vi,_vg,_vs,_a,_q,_e,_p,_peri,_node,_incl,'\ - '_stream,_mag,_dur,_lng1,_lat1,_H1,_lng2,_lat2,_H2,_LD21,_az1r,_ev1r,_Nts,_Nos,_leap,_tme,_dt,'\ - 'dtstamp,orbname,iau,name,mass,pi,Q,true_anom,EA,MA,Tj,T,last_peri,jacchia1,Jacchia2,numstats,stations' - expr = f"SELECT {fieldlist} from s3object s where s._localtime like '_{reqval}%'" - fhi = {"FileHeaderInfo": "Use"} - elif reqtyp == 'matches': - d1 = datetime.datetime.strptime(reqval, '%Y%m%d') - idxfile = 'matches/matched/matches-full-{:04d}.csv'.format(d1.year) - res = '{"no matches"}' - expr = "SELECT s.orbname from s3object s where s._localtime like '_{}%'".format(reqval) - fhi = {"FileHeaderInfo": "Use"} - elif reqtyp == 'detail': - idxfile = 'matches/matched/matches-full-{}.csv'.format(reqval[:4]) - res = '{"event not found"}' - expr = "SELECT * from s3object s where s.orbname='{}'".format(reqval) - fhi = {"FileHeaderInfo": "Use"} - elif reqtyp == 'station': - statid = qs['statid'] - d1 = datetime.datetime.strptime(reqval, '%Y%m%d') - idxfile = 'matches/matched/matches-full-{:04d}.csv'.format(d1.year) - res = '{"no matches"}' - expr = "SELECT s.orbname from s3object s where s._localtime like '_{}%' and s.stations like '%{}%'".format(reqval, statid) - fhi = {"FileHeaderInfo": "Use"} - - s3 = boto3.client('s3') - resp = s3.select_object_content(Bucket=target, Key=idxfile, ExpressionType='SQL', - Expression=expr, InputSerialization={'CSV': fhi, 'CompressionType': 'NONE'}, OutputSerialization={'JSON': {}}, ) - res='' - for event in resp['Payload']: - if 'Records' in event: - res = res + event['Records']['Payload'].decode('utf-8') - if reqtyp == 'summary': - res = '[' + res.replace('}\n{','},\n{') +']' - if points is True: - print('requested points') - url = json.loads(res)['img'] - url = url.replace('ground_track.png','report.txt') - reppth = url.split('//', 1)[1].split('/',1)[1] - _, repname = os.path.split(reppth) - try: - tmpfname = f'/tmp/{repname}' - s3.download_file(webbuck, reppth, tmpfname) - flis = open(tmpfname, 'r').readlines() - os.remove(tmpfname) - res = fileToJsonString(flis) - except Exception: - print('report file unavailable') - res = '{"points": "unavailable"}' - - return { - 'statusCode': 200, - 'body': res - } - - -def fileToJsonString(flis): - hdr = ['No','statid','ign','t','jd','m1','m2','az','alt','azl','altl','rao', - 'deco','ral','decl','X','Y','Z','lat','lon','H','range','length','svd', - 'lag','vel','pvel', 'hres','vres','ares','vmag','amag'] - ptsarray='[' - gotpts = False - for fli in flis: - if 'Points' in fli: - gotpts = True - continue - elif '------' in fli or ' No' in fli: - continue - elif gotpts is True and (len(fli) < 2 or 'Notes' in fli): - gotpts=False - break - elif gotpts is True: - spls = fli.split(',') - thisrow = '{' - for h, s in zip(hdr, spls): - thisrow = thisrow + f'"{h}": "{s.strip()}",' - thisrow = thisrow[:-1] + '},' - ptsarray = ptsarray + thisrow - ptsarray = ptsarray[:-1] + ']' - ptsarray = '{' + f'"points": {ptsarray}' + '}' - return ptsarray diff --git a/archive/terraform/ukmda/files/policies/ukmda-calcserverpolicy.json b/archive/terraform/ukmda/files/policies/ukmda-calcserverpolicy.json index 2dd3c0d01..8b772ae2b 100644 --- a/archive/terraform/ukmda/files/policies/ukmda-calcserverpolicy.json +++ b/archive/terraform/ukmda/files/policies/ukmda-calcserverpolicy.json @@ -36,7 +36,8 @@ "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply", - "ses:SendEmail" + "ses:SendEmail", + "sts:GetCallerIdentity" ], "Effect": "Allow", "Resource": [ diff --git a/archive/terraform/ukmda/loggingbucket.tf b/archive/terraform/ukmda/loggingbucket.tf index 0bab24ff9..adbffcf74 100644 --- a/archive/terraform/ukmda/loggingbucket.tf +++ b/archive/terraform/ukmda/loggingbucket.tf @@ -10,12 +10,14 @@ resource "aws_s3_bucket" "logbucket" { resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp" { bucket = aws_s3_bucket.logbucket.id + transition_default_minimum_object_size = "all_storage_classes_128K" rule { status = "Enabled" id = "purge old versions" noncurrent_version_expiration { noncurrent_days = 30 } + filter {} } rule { id = "purge old ukmda-shared logs" @@ -23,7 +25,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp" { expiration { days = 10 - expired_object_delete_marker = false } filter { @@ -40,11 +41,10 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp" { expiration { days = 10 - expired_object_delete_marker = false } filter { - prefix = "archsite/" + prefix = "website/" } noncurrent_version_expiration { @@ -57,7 +57,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp" { expiration { days = 10 - expired_object_delete_marker = false } filter { @@ -106,12 +105,14 @@ resource "aws_s3_bucket" "logbucket_w1" { resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp_w1" { bucket = aws_s3_bucket.logbucket_w1.id provider = aws.eu-west-1-prov + transition_default_minimum_object_size = "varies_by_storage_class" rule { status = "Enabled" id = "purge old versions" noncurrent_version_expiration { noncurrent_days = 30 } + filter {} } rule { id = "purge old ukmda-live logs" @@ -119,7 +120,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdalogslcp_w1" { expiration { days = 10 - expired_object_delete_marker = false } filter { diff --git a/archive/terraform/ukmda/matchApiLambda.tf b/archive/terraform/ukmda/matchApiLambda.tf deleted file mode 100644 index 30348402c..000000000 --- a/archive/terraform/ukmda/matchApiLambda.tf +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2018-2023 Mark McIntyre -# Lambda for the Match data API. Need to rework this. -# -data "archive_file" "matchapizip" { - type = "zip" - source_dir = "${path.root}/files/matchDataApi/" - output_path = "${path.root}/files/matchDataApi.zip" -} - -resource "aws_lambda_function" "matchapilambda" { - function_name = "matchDataApiHandler" - description = "API to retrieve match event data" - filename = data.archive_file.matchapizip.output_path - source_code_hash = data.archive_file.matchapizip.output_base64sha256 - handler = "matchDataApiHandler.lambda_handler" - runtime = "python3.11" - memory_size = 128 - timeout = 30 - role = aws_iam_role.S3FullAccess.arn - publish = false - environment { - variables = { - "SRCHBUCKET" = "${var.sharedbucket}" - } - } - ephemeral_storage { - size = 512 - } - tags = { - Name = "matchDataApiHandler" - billingtag = "ukmda" - } -} - -data "template_file" "match_body_templ" { - template = file("files/matchApiJson/ukmdaMatchApi.json") - vars = { - thisacct = "${data.aws_caller_identity.current.account_id}" - } -} - - -resource "aws_api_gateway_rest_api" "matchapi_apigateway" { - body = data.template_file.match_body_templ.rendered # file("files/matchApiJson/ukmdaMatchApi.json") - name = "ukmdaMatchApi" - provider = aws.eu-west-1-prov - endpoint_configuration { - types = ["REGIONAL"] - } - tags = { - Name = "ukmdaMatchApi" - billingtag = "ukmda" - } -} - -resource "aws_api_gateway_base_path_mapping" "matchapi" { - api_id = aws_api_gateway_rest_api.matchapi_apigateway.id - stage_name = "prod" - domain_name = aws_api_gateway_domain_name.apigwdomain.domain_name - base_path = "matches" - provider = aws.eu-west-1-prov - depends_on = [aws_api_gateway_stage.matchapistage] -} - -resource "aws_api_gateway_stage" "matchapistage" { - deployment_id = aws_api_gateway_deployment.matchapi_deployment.id - rest_api_id = aws_api_gateway_rest_api.matchapi_apigateway.id - stage_name = "prod" - provider = aws.eu-west-1-prov -} - -resource "aws_api_gateway_deployment" "matchapi_deployment" { - rest_api_id = aws_api_gateway_rest_api.matchapi_apigateway.id - provider = aws.eu-west-1-prov - triggers = { - redeployment = sha1(jsonencode(aws_api_gateway_rest_api.matchapi_apigateway.body)) - } - - lifecycle { - create_before_destroy = true - } -} - -resource "aws_lambda_permission" "perm_apigw_match" { - statement_id = "AllowExecutionFromAPIGW" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.matchapilambda.arn - principal = "apigateway.amazonaws.com" - source_arn = "arn:aws:execute-api:eu-west-2:183798037734:975kbpqxg6/*/GET/" - #source_arn = "${aws_api_gateway_rest_api.matchapi_apigateway.arn}/*/*" -} diff --git a/archive/terraform/ukmda/ssm_variables.tf b/archive/terraform/ukmda/ssm_variables.tf new file mode 100644 index 000000000..29fba5ac6 --- /dev/null +++ b/archive/terraform/ukmda/ssm_variables.tf @@ -0,0 +1,43 @@ +# Copyright (C) 2018-2023 Mark McIntyre + +# SSM parameters for use in Lambdas + +resource "aws_ssm_parameter" "prod_dbhost" { + provider = aws.eu-west-1-prov + name = "prod_dbhost" + type = "String" + value = "3.11.55.160" + tags = { + "billingtag" = "ukmon" + } +} + +resource "aws_ssm_parameter" "prod_dbname" { + provider = aws.eu-west-1-prov + name = "prod_dbname" + type = "String" + value = "ukmon" + tags = { + "billingtag" = "ukmon" + } +} + +resource "aws_ssm_parameter" "prod_dbpw" { + provider = aws.eu-west-1-prov + name = "prod_dbpw" + type = "SecureString" + value = "Batch33mdl" + tags = { + "billingtag" = "ukmon" + } +} + +resource "aws_ssm_parameter" "prod_dbuser" { + provider = aws.eu-west-1-prov + name = "prod_dbuser" + type = "String" + value = "batch" + tags = { + "billingtag" = "ukmon" + } +} diff --git a/archive/terraform/ukmda/ukmdaadmin_bucket.tf b/archive/terraform/ukmda/ukmdaadmin_bucket.tf index c588da602..a36f9a433 100644 --- a/archive/terraform/ukmda/ukmdaadmin_bucket.tf +++ b/archive/terraform/ukmda/ukmdaadmin_bucket.tf @@ -3,17 +3,18 @@ resource "aws_s3_bucket" "adminbucket" { force_destroy = false tags = { "billingtag" = "ukmda" + "ukmdatype" = "admin" } } resource "aws_s3_bucket_lifecycle_configuration" "adminbucketlcp" { bucket = aws_s3_bucket.adminbucket.id + transition_default_minimum_object_size = "varies_by_storage_class" rule { id = "purge old emails" status = "Enabled" expiration { days = 30 - expired_object_delete_marker = false } filter { prefix = "email/" diff --git a/archive/terraform/ukmda/ukmdalive_bucket.tf b/archive/terraform/ukmda/ukmdalive_bucket.tf index 2889ce61c..7e1c405bb 100644 --- a/archive/terraform/ukmda/ukmdalive_bucket.tf +++ b/archive/terraform/ukmda/ukmdalive_bucket.tf @@ -65,6 +65,7 @@ resource "aws_s3_bucket_policy" "ukmdalivebp" { resource "aws_s3_bucket_lifecycle_configuration" "ukmdalivelcp" { bucket = aws_s3_bucket.ukmdalive.id provider = aws.eu-west-1-prov + transition_default_minimum_object_size = "varies_by_storage_class" rule { status = "Enabled" id = "1 months delete" @@ -76,8 +77,8 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdalivelcp" { } expiration { days = 30 - expired_object_delete_marker = false } + filter {} } } diff --git a/archive/terraform/ukmda/ukmdashared_bucket.tf b/archive/terraform/ukmda/ukmdashared_bucket.tf index 7e992f090..bad938fe0 100644 --- a/archive/terraform/ukmda/ukmdashared_bucket.tf +++ b/archive/terraform/ukmda/ukmdashared_bucket.tf @@ -24,9 +24,9 @@ resource "aws_s3_bucket_policy" "ukmdasharedbp" { Effect = "Allow" Principal = { AWS = [ - "arn:aws:iam::${var.remote_account_id}:role/S3FullAccess", # role used by lambdas + "arn:aws:iam::${var.remote_account_id}:role/S3FullAccess", # role used by lambdas "arn:aws:iam::${var.remote_account_id}:role/lambda-s3-full-access-role", # role used by SAM functions - "arn:aws:iam::${var.remote_account_id}:role/ecsTaskExecutionRole", # role used by ECS tasks + "arn:aws:iam::${var.remote_account_id}:role/ecsTaskExecutionRole", # role used by ECS tasks "arn:aws:iam::${var.remote_account_id}:user/Mary", "arn:aws:iam::${var.remote_account_id}:user/Mark" ] @@ -38,22 +38,22 @@ resource "aws_s3_bucket_policy" "ukmdasharedbp" { Sid = "DelegateS3Access" }, { - "Sid": "BlockAccessToAdmin", - "Effect": "Deny", - "Principal": "*", - "Action": "s3:*", - "Resource": "arn:aws:s3:::ukmda-shared/admin/*", - "Condition": { - "StringNotLike": { - "aws:userId": [ # update with new relevant IDs - "AIDASVSZXPTTB3UZT4E2B", # MarkMcIntyreUKM - "AROAUUCG4WH4GFCTQIKH3", # S3FullAccess in MJMM account - "AROAUUCG4WH4GFCTQIKH3:*", # S3FullAccess in MJMM account - "${data.aws_caller_identity.current.account_id}", # root account - "AROA36ZZGKDHYW6XYFNJD:*" - ] - } + "Sid" : "BlockAccessToAdmin", + "Effect" : "Deny", + "Principal" : "*", + "Action" : "s3:*", + "Resource" : "arn:aws:s3:::ukmda-shared/admin/*", + "Condition" : { + "StringNotLike" : { + "aws:userId" : [ # update with new relevant IDs + "AIDASVSZXPTTB3UZT4E2B", # MarkMcIntyreUKM + "AROAUUCG4WH4GFCTQIKH3", # S3FullAccess in MJMM account + "AROAUUCG4WH4GFCTQIKH3:*", # S3FullAccess in MJMM account + "${data.aws_caller_identity.current.account_id}", # root account + "AROA36ZZGKDHYW6XYFNJD:*" + ] } + } } ] Version = "2012-10-17" @@ -79,12 +79,14 @@ resource "aws_s3_bucket_acl" "ukmdasharedacl" { resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { bucket = aws_s3_bucket.ukmdashared.id + transition_default_minimum_object_size = "all_storage_classes_128K" rule { status = "Enabled" id = "purge old versions" noncurrent_version_expiration { noncurrent_days = 30 } + filter {} } rule { status = "Enabled" @@ -94,7 +96,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { } transition { - days = 45 + days = 30 storage_class = "STANDARD_IA" } } @@ -104,7 +106,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { expiration { days = 2 - expired_object_delete_marker = false } filter { @@ -121,7 +122,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { expiration { days = 30 - expired_object_delete_marker = false } filter { @@ -138,7 +138,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { expiration { days = 60 - expired_object_delete_marker = false } filter { @@ -155,7 +154,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { expiration { days = 30 - expired_object_delete_marker = false } filter { @@ -172,7 +170,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { expiration { days = 30 - expired_object_delete_marker = false } filter { @@ -183,6 +180,18 @@ resource "aws_s3_bucket_lifecycle_configuration" "ukmdasharedlcp" { noncurrent_days = 10 } } + rule { + status = "Enabled" + id = "Transition to Glacier" + filter { + prefix = "archive/" + } + + transition { + days = 180 + storage_class = "GLACIER_IR" + } + } } diff --git a/archive/terraform/ukmda/ukmdawebsite_bucket.tf b/archive/terraform/ukmda/ukmdawebsite_bucket.tf index 17e9083b1..e1d2de677 100644 --- a/archive/terraform/ukmda/ukmdawebsite_bucket.tf +++ b/archive/terraform/ukmda/ukmdawebsite_bucket.tf @@ -4,6 +4,7 @@ resource "aws_s3_bucket" "archsite" { force_destroy = false tags = { "billingtag" = "ukmda" + "ukmdatype" = "website" } } @@ -86,12 +87,14 @@ resource "aws_s3_bucket_logging" "ukmdawebsitelogs" { resource "aws_s3_bucket_lifecycle_configuration" "archsitelcp" { bucket = aws_s3_bucket.archsite.id + transition_default_minimum_object_size = "varies_by_storage_class" rule { status = "Enabled" id = "purge old versions" noncurrent_version_expiration { noncurrent_days = 30 } + filter {} } rule { status = "Enabled" diff --git a/archive/terraform/ukmda/vpc.tf b/archive/terraform/ukmda/vpc.tf index 79ba622d4..455e6c3a8 100644 --- a/archive/terraform/ukmda/vpc.tf +++ b/archive/terraform/ukmda/vpc.tf @@ -73,11 +73,16 @@ resource "aws_default_route_table" "ec2_rtbl" { vpc_peering_connection_id = aws_vpc_peering_connection.ukmdatommpeering.id } tags = { - "Name" = "ec2_rtbl" + "Name" = "public_rtbl" billingtag = "Management" } } +resource "aws_route_table_association" "ec2_rtbl_assoc" { + subnet_id = aws_subnet.ec2_subnet.id + route_table_id = aws_default_route_table.ec2_rtbl.id +} + # internet gateway for this account resource "aws_internet_gateway" "main_igw" { vpc_id = aws_vpc.ec2_vpc.id diff --git a/archive/ukmon_pylib/analysis/analyseGmnData.py b/archive/ukmon_pylib/analysis/analyseGmnData.py new file mode 100644 index 000000000..77920b99c --- /dev/null +++ b/archive/ukmon_pylib/analysis/analyseGmnData.py @@ -0,0 +1,20 @@ +# simple scripts to analyse the GMN data in python + +import pandas as pd +import os + +from converters.gmnTxtToPandas import dirpath + + +def findDuplicates(yr, mth=None): + if mth: + datafile = os.path.join(dirpath, 'parquet', 'monthly', f'gmn_{yr:04d}{mth:02d}.parquet.snap') + else: + datafile = os.path.join(dirpath, 'parquet', f'gmn_{yr:04d}.parquet.snap') + df = pd.read_parquet(datafile) + df['dupe']=df.duplicated(subset=['id']) + dupeids = df[df.dupe].sort_values(by=['id']).id + duperows = df[df.id.isin(dupeids)] + print(duperows) + print(len(df)) + return duperows diff --git a/archive/ukmon_pylib/analysis/findCamerasNearPoint.py b/archive/ukmon_pylib/analysis/findCamerasNearPoint.py deleted file mode 100644 index 8f786999d..000000000 --- a/archive/ukmon_pylib/analysis/findCamerasNearPoint.py +++ /dev/null @@ -1,49 +0,0 @@ -# copyright Mark McIntyre, 2024- - -# find cameras near to a given point (lat/lon in degrees) - -import os -import sys -import json -import numpy as np -import requests - -from meteortools.utils.Math import greatCircleDistance - -def camerasNearTo(lat, lon, distFromPt): - l1 = np.radians(lat) - g1 = np.radians(lon) - cams = [] - datadir = os.getenv('DATADIR', default='/home/ec2-user/prod/data') - camdb = os.path.join(datadir, 'admin', 'cameraLocs.json') - caminfo = json.load(open(camdb)) - for cam in caminfo: - l2 = np.radians(caminfo[cam]['lat']) - g2 = np.radians(caminfo[cam]['lon']) - dist = greatCircleDistance(l1, g1, l2, g2) - # print(dist) - if dist < distFromPt: - url = f'https://api.ukmeteors.co.uk/camdetails?camid={cam}' - res = requests.get(url) - if res.status_code == 200: - dta = json.loads(res.text) - cams.append(dta[0]['eMail']) - cams = list(set(cams)) - camstr = '' - for cam in cams: - camstr = camstr + cam + ';' - return camstr - - -def getCoordsFromTraj(trajname): - lat = 0 - lon = 0 - return lat, lon - - -if __name__ == '__main__': - lat = float(sys.argv[1]) - lon = float(sys.argv[2]) - dist = float(sys.argv[3]) - - print(camerasNearTo(lat, lon, dist)) \ No newline at end of file diff --git a/archive/ukmon_pylib/analysis/gatherDetectionData.py b/archive/ukmon_pylib/analysis/gatherDetectionData.py index eeaed0a60..b9031167b 100644 --- a/archive/ukmon_pylib/analysis/gatherDetectionData.py +++ b/archive/ukmon_pylib/analysis/gatherDetectionData.py @@ -6,6 +6,7 @@ import os import sys import datetime +import pandas as pd from meteortools.ukmondb import getECSVs, getLiveJpgs, getFBfiles, getDetections @@ -35,6 +36,21 @@ def getRawData(idlist, outpth): return +def updateSingleDB(yr, consdt): + datadir = os.getenv('DATADIR', default='/home/ec2-user/prod/data') + sngls = pd.read_csv(os.path.join(datadir, 'single', f'singles-{yr}.csv')) + consdf = pd.read_csv(os.path.join(datadir, 'single', 'used', f'consumed_{consdt}.txt'), names=['Filename']) + conslist = list(consdf.Filename) + for ff in conslist: + mtch = sngls.Filename == ff + if mtch.any(): + print(f'updating {ff}') + sngls.loc[mtch, 'status'] = 'Y' + else: + print(f'skipping {ff}') + sngls.to_csv(os.path.join(datadir, 'single', f'singles-{yr}.csv'), index=False) + + if __name__ == '__main__': outpth = f'./{sys.argv[1]}' os.makedirs(outpth, exist_ok=True) diff --git a/archive/ukmon_pylib/converters/gmnTxtToPandas.py b/archive/ukmon_pylib/converters/gmnTxtToPandas.py index e897c7c4e..ad98886f9 100644 --- a/archive/ukmon_pylib/converters/gmnTxtToPandas.py +++ b/archive/ukmon_pylib/converters/gmnTxtToPandas.py @@ -5,6 +5,7 @@ import os import glob import sys +import datetime colhdrs = ['id','jd_beg','utc_beg','iau_no','iau_code','sollon','lst', @@ -16,6 +17,9 @@ 'Lat1','Lat1sd','Lon1','Lon1sd','H1','H1sd','Lat2','Lat2sd','Lon2','Lon2sd','H2','H2sd', 'Dur','Amag','PkHt','F1','mass','Qc','MedianFitErr','BegIn','EndIn','NumStat','stats'] +datadir=os.getenv('DATADIR', default='/home/ec2-user/prod/data') +dirpath = os.path.join(datadir, 'gmndata') + def loadOneFile(fname): print(f'processing {fname}') @@ -45,14 +49,92 @@ def loadOneFile(fname): return df +def compareTwoFiles(file1, file2): + df1 = loadOneFile(file1) + df2 = loadOneFile(file2) + + df1['dupe']=df1.duplicated(subset=['id']) + print(f'file 1 contains {len(df1[df1.dupe==True].id.unique())} duplicates') + dupe1 = df1[df1.dupe==True] # noqa: E712 + df1.drop(columns=['dupe']) + + df2['dupe']=df2.duplicated(subset=['id']) + print(f'file 2 contains {len(df2[df2.dupe==True].id.unique())} duplicates') + dupe2 = df2[df2.dupe==True] # noqa: E712 + df2.drop(columns=['dupe']) + + mrg = df1.merge(df2.drop_duplicates(), on=['id'], how='left', indicator=True) + old_not_new = mrg[mrg._merge!='both'] + print(f'there are {len(old_not_new)} rows in the first file not in the second') + + mrg = df2.merge(df1.drop_duplicates(), on=['id'], how='left', indicator=True) + new_not_old = mrg[mrg._merge!='both'] + print(f'there are {len(new_not_old)} rows in the second file not in the first') + + print(old_not_new) + old_not_new.to_csv('./old_not_new.csv') + new_not_old.to_csv('./old_not_new.csv') + return df1,df2, old_not_new, new_not_old, dupe1, dupe2 + + +def aggregateOneMonth(yr, mth): + startdt = datetime.datetime(yr, mth, 1) - datetime.timedelta(days=1) + syr = startdt.year + smth = startdt.month + sday = startdt.day + datafiles = glob.glob(os.path.join(dirpath, 'daily', f'traj_summary_{syr:04d}{smth:02d}{sday:02d}*.txt')) + prevfile = datafiles[0] + datafiles = [prevfile] + glob.glob(os.path.join(dirpath, 'daily', f'traj_summary_{yr:04d}{mth:02d}*.txt')) + mthlydata = None + for datfile in datafiles: + newdata = loadOneFile(datfile) + if mthlydata is None: + mthlydata = newdata + else: + mthlydata = pd.concat([mthlydata, newdata], sort=True) + mthlydata = mthlydata[mthlydata.utc_beg >= datetime.datetime(yr,mth,1,0,0,0)] + return mthlydata + + def doYear(yr): - dirpath='F:/videos/MeteorCam/gmndata' - datafiles = glob.glob(os.path.join(dirpath, f'traj_summary_monthly_{yr}*.txt')) + datafiles = glob.glob(os.path.join(dirpath, 'monthly', f'traj_summary_monthly_{yr}*.txt')) for datfile in datafiles: + print(f'processing {datfile}') newdata = loadOneFile(datfile) fn, _ = os.path.splitext(datfile) ym = fn[-6:] - newdata.to_parquet(os.path.join(dirpath, 'parquet', f'gmn_{ym}.parquet.snap'), index=False) + newdata.to_parquet(os.path.join(dirpath, 'parquet', 'monthly', f'gmn_{ym}.parquet.snap'), index=False) + + datafiles = glob.glob(os.path.join(dirpath, f'traj_summary_yearly_{yr}.txt')) + for datfile in datafiles: + print(f'processing {datfile}') + newdata = loadOneFile(datfile) + newdata.to_parquet(os.path.join(dirpath, 'parquet', f'gmn_{yr}.parquet.snap'), index=False) + return + + +def getStats(): + for yr in range(2022,2025): + df = pd.read_parquet(f'gmn_{yr}01.parquet.snap') + for mth in range(2,13): + try: + df2 = pd.read_parquet(f'gmn_{yr}{mth:02d}.parquet.snap') + df = pd.concat(df, df2) + except: + pass +# df = df[df.Amag < -3.99] + print(f'{yr}, {len(df)}, {df.Lat1.mean():.1f}, {df.Lat1.max():.1f}, {df.Lat1.min():.1f}, {df.Lat1.std():.1f}') + print('-----') + for yr in range(2022,2025): + df = pd.read_parquet(f'gmn_{yr}01.parquet.snap') + for mth in range(2,13): + try: + df2 = pd.read_parquet(f'gmn_{yr}{mth:02d}.parquet.snap') + df = pd.concat(df, df2) + except: + pass + df = df[df.Amag < -3.99] + print(f'{yr}, {len(df)}, {df.Lat1.mean():.1f}, {df.Lat1.max():.1f}, {df.Lat1.min():.1f}, {df.Lat1.std():.1f}') if __name__ == '__main__': diff --git a/archive/ukmon_pylib/maintenance/compressOldArchiveData.py b/archive/ukmon_pylib/maintenance/compressOldArchiveData.py new file mode 100644 index 000000000..ed04fced5 --- /dev/null +++ b/archive/ukmon_pylib/maintenance/compressOldArchiveData.py @@ -0,0 +1,345 @@ +# copyright 2023- Mark McIntyre +# all rights reserved + +""" + a set of functions to + - compress the ukmda-shared/archive data from before the current year + - remove unused but easily recreated files from the website + - remove links to the old downloadable zip file + +usage: + 1) python compressOldArchiveData.py - will scan and compress ALL data from prior years + 2) python compressOldArchiveData.py "archive/somefolder" - will scan and compress only the named location folder + 3) python compressOldArchiveData.py prune_website - will scan and remove unused files from the website + 3) python compressOldArchiveData.py prune_website reports/2024/202401 - will scan and remove unused files + from the named location and below + +note that any MP4 or AVI files that have been uploaded to this area are removed by this process. The videos should be +on the website in ukmda-website/img/mp4s in mp4 format +""" + +import boto3 +import zipfile +import io +import os +import datetime +import argparse + +maxyear = datetime.datetime.now().year - 1 + +s3 = boto3.client('s3') +s3res = boto3.resource('s3') + + +def listLocations(srcbucket='ukmda-shared', prefixstr='archive/'): + """ + list all top level folders in a particular prefix + parameters: + srcbucket: [string] bucket name, default ukmda-shared + prefixstr: [string] prefix, default archive/ - note must have terminal slash + + returns: + list of prefixes + """ + response = s3.list_objects_v2(Bucket=srcbucket, Prefix=prefixstr, Delimiter='/') + if 'CommonPrefixes' in response: + return response['CommonPrefixes'] + else: + return [] + + +def listCameras(srcbucket='ukmda-shared', location='archive/Tackley/'): + response = s3.list_objects_v2(Bucket=srcbucket, Prefix=location, Delimiter='/') + if 'CommonPrefixes' in response: + return response['CommonPrefixes'] + else: + return [] + + +def listYears(srcbucket='ukmda-shared', camid='archive/Tackley/c1/'): + response = s3.list_objects_v2(Bucket=srcbucket, Prefix=camid, Delimiter='/') + if 'CommonPrefixes' in response: + return response['CommonPrefixes'] + else: + return [] + + +""" +Not used +def getAllS3Objects(s3, **base_kwargs): + continuation_token = None + while True: + list_kwargs = dict(MaxKeys=1000, **base_kwargs) + if continuation_token: + list_kwargs['ContinuationToken'] = continuation_token + response = s3.list_objects_v2(**list_kwargs) + yield from response.get('Contents', []) + if not response.get('IsTruncated'): # At the end of the list? + break + continuation_token = response.get('NextContinuationToken') +""" + + +def deleteFiles(flist, srcbucket='ukmda-shared'): + chunk_size = 900 + chunked_list = [flist[i:i + chunk_size] for i in range(0, len(flist), chunk_size)] + for ch in chunked_list: + delete_keys = {'Objects': []} + delete_keys['Objects'] = [{'Key': k} for k in ch] + s3.delete_objects(Bucket=srcbucket, Delete=delete_keys) + return + + +def compressObjects(srcbucket='ukmda-shared', camprefix=None): + lcmprefixes = [] + if camprefix is None: + locations = listLocations() + for location in locations: + camids = listCameras(location=location['Prefix']) + for camid in camids: + years = listYears(camid=camid['Prefix']) + for year in years: + lcmprefix = year['Prefix'][:-1] + lcmprefixes.append(lcmprefix) + else: + spls = camprefix[:-1].split('/') + print(spls) + if len(spls) < 3: + camids = listCameras(location=camprefix) + for camid in camids: + years = listYears(camid=camid['Prefix']) + for year in years: + lcmprefix = year['Prefix'][:-1] + lcmprefixes.append(lcmprefix) + else: + years = listYears(camid=camprefix) + for year in years: + lcmprefix = year['Prefix'][:-1] + lcmprefixes.append(lcmprefix) + + for lcmprefix in lcmprefixes: + spls = lcmprefix.split('/') + zfname = f'{spls[0]}/{spls[1]}/{spls[1]}_{spls[2]}_{spls[3]}.zip' + try: + if int(spls[3]) > maxyear or 'miscellan' in spls[3] or 'profiles' in spls[3]: + continue + except Exception: + pass + if 'testpi' in lcmprefix: + continue + bucket = s3res.Bucket(srcbucket) + files = [os.key for os in bucket.objects.filter(Prefix=lcmprefix)] + mp4files = [file for file in files if '.mp4' in file] + deleteFiles(mp4files) + avifiles = [file for file in files if '.avi' in file] + deleteFiles(avifiles) + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: + for key in files: + if '.mp4' not in key and '.avi' not in key: + print(key) + data = s3.get_object(Bucket=srcbucket, Key=key) + content = data['Body'].read() + zipf.writestr(key, content) + + # Upload the zip file to the target bucket + zip_buffer.seek(0) + print(f'uploading {zfname}') + s3.upload_fileobj(zip_buffer, srcbucket, zfname) + # now delete the files + print(f'deleting files from {lcmprefix}') + deleteFiles(files) + + +def moveJpgsAndMp4s(source_bucket, yr, ym): + print(f'moving jpgs and mp4s from {ym}') + bucket = s3res.Bucket(source_bucket) + files = [os.key for os in bucket.objects.filter(Prefix=f'reports/{yr}/orbits/{ym}/')] + jpgs = [x for x in files if '.jpg' in x or '.mp4' in x] + print(f'moving {len(jpgs)} jpgs or mp4s') + for jpg in jpgs: + print(jpg) + keyname = os.path.split(jpg)[1] + if '.jpg' in keyname: + newkey = f'img/single/{yr}/{ym}/{keyname}' + else: + newkey = f'img/mp4/{yr}/{ym}/{keyname}' + s3.copy_object(Bucket=source_bucket, + CopySource={'Bucket':source_bucket,'Key':jpg}, + Key=newkey, MetadataDirective='COPY') + s3.delete_object(Bucket=source_bucket, Key=jpg) + jpgfldrs = [os.path.split(x)[0] for x in jpgs] + jpgfldrs = list(set(jpgfldrs)) + jpgfldrs.sort() + print(f'updating {len(jpgfldrs)} indexes') + for fldr in jpgfldrs: + print(fldr) + idx = f'{fldr}/index.html' + rootdir = os.path.split(idx)[0] + try: + s3.download_file(source_bucket, idx, '/tmp/oldindex.html') + except Exception: + continue + lis = open('/tmp/oldindex.html', 'r').readlines() + # skip files that dont have the line in them + mtch = [li for li in lis if '.jpg' in li or '.mp4' in li] + if len(mtch) == 0: + continue + with open('/tmp/newindex.html', 'w') as outf: + for li in lis: + if '.jpg' in li and '/img/single/' not in li: + newpth=f'/img/single/{yr}/{ym}/' + li = li.replace('a href="', f'a href="{newpth}') + li = li.replace('img src="', f'img src="{newpth}') + if '.mp4' in li and '/img/mp4/' not in li: + newpth=f'/img/mp4/{yr}/{ym}/' + li = li.replace('a href="', f'a href="{newpth}') + li = li.replace('source src="', f'source src="{newpth}') + li = li.replace(rootdir, '') + li = li.replace('//', '/') + li = li.replace('//', '/') + outf.write(li) + extraargs = {'ContentType': 'text/html'} + s3.upload_file('/tmp/newindex.html', source_bucket, idx, ExtraArgs=extraargs) + print('done') + + +def fixBrokenIndexes(source_bucket, yr, ym): + bucket = s3res.Bucket(source_bucket) + files = [os.key for os in bucket.objects.filter(Prefix=f'reports/{yr}/orbits/{ym}/')] + idxs = [x for x in files if 'index.html' in x] + for idx in idxs: + print(idx) + s3.download_file(source_bucket, idx, '/tmp/oldindex.html') + lis = open('/tmp/oldindex.html', 'r').readlines() + rootdir = os.path.split(idx)[0] + # skip files that dont have the line in them + mtch = [li for li in lis if '.jpg' in li or '.mp4' in li] + if len(mtch) == 0: + continue + with open('/tmp/newindex.html', 'w') as outf: + for li in lis: + if '.jpg' in li and '/img/single/' not in li: + newpth=f'/img/single/{yr}/{ym}/' + li = li.replace('a href="', f'a href="{newpth}') + li = li.replace('img src="', f'img src="{newpth}') + if '.mp4' in li and '/img/mp4/' not in li: + newpth=f'/img/mp4/{yr}/{ym}/' + li = li.replace('a href="', f'a href="{newpth}') + li = li.replace('source src="', f'source src="{newpth}') + li = li.replace(rootdir, '') + li = li.replace('//', '/') + li = li.replace('//', '/') + outf.write(li) +# for li in lis: +# if '.jpg' in li: +# oldpth = f'/img/single/{yr}/{ym}' +# newpth = f'/img/single/{yr}/{ym}/' +# li = li.replace(f'a href="{oldpth}', f'a href="{newpth}') +# li = li.replace(f'img src="{oldpth}', f'img src="{newpth}') +# if '.mp4' in li: +# oldpth = f'/img/single/{yr}/{ym}' +# newpth=f'/img/mp4/{yr}/{ym}/' +# li = li.replace(f'a href="{oldpth}', f'a href="{newpth}') +# li = li.replace(f'source src="{oldpth}', f'source src="{newpth}') +# li = li.replace(rootdir, '') +# li = li.replace('//', '/') +# li = li.replace('//', '/') +# outf.write(li) + extraargs = {'ContentType': 'text/html'} + s3.upload_file('/tmp/newindex.html', source_bucket, idx, ExtraArgs=extraargs) + + +def updateIndexes(idxs, source_bucket): + for idx in idxs: + if len(idx.split('/')) < 7: + continue + s3.download_file(source_bucket, idx, '/tmp/oldindex.html') + lis = open('/tmp/oldindex.html', 'r').readlines() + # skip files that dont have the line in them + mtch = [li for li in lis if 'download a zip of the' in li] + if len(mtch) == 0: + continue + with open('/tmp/newindex.html', 'w') as outf: + for li in lis: + if 'download a zip of the' in li: + continue + outf.write(li) + extraargs = {'ContentType': 'text/html'} + s3.upload_file('/tmp/newindex.html', source_bucket, idx, ExtraArgs=extraargs) + return + + +def pruneObjects(source_bucket, prefix_str, force_reindex=False): + spls = prefix_str.split('/') + bucket = s3res.Bucket(source_bucket) + if len(spls) < 3: + years = listYears(source_bucket, prefix_str) + years = [x['Prefix'] for x in years if 'reports/20' in x['Prefix']] + else: + years = [f'reports/{spls[1]}/'] + for yr in years: + print(f'processing {yr}') + yr_prefix = f'{yr}orbits/' + if len(spls) > 3: + mths = [f'{yr_prefix}{spls[2]}/'] + else: + mths = listYears(source_bucket, yr_prefix) + mths = [x['Prefix'] for x in mths if 'csv/' not in x['Prefix'] and 'plots/' not in x['Prefix']] + for mth in mths: + print(f'processing {mth}') + files = [os.key for os in bucket.objects.filter(Prefix=mth)] + print('purging zip files') + zipfs = [file for file in files if '.zip' in file] + deleteFiles(zipfs, source_bucket) + if len(zipfs) > 0 or force_reindex: + print('updating indexes') + idxs = [file for file in files if '/index.html' in file] + updateIndexes(idxs, source_bucket) + print('purging pngs') + spatres = [file for file in files if '_spatial_residuals.png' in file and '_all' not in file] + deleteFiles(spatres, source_bucket) + print('purging kml and ftpdetect') + others = [file for file in files if '.kml' in file or 'FTPdetectinfo' in file] + deleteFiles(others, source_bucket) + print('purging csv') + csvs = [file for file in files if '.csv' in file] + deleteFiles(csvs, source_bucket) + return + + +if __name__ == '__main__': + prefix_str = None + + arg_parser = argparse.ArgumentParser(description='Compress, prune or clear down older data') + + arg_parser.add_argument('command_str', nargs=1, metavar='COMMAND_STR', type=str, + help='action to take. Options are COMPRESS or PRUNE to compress archive files or prune the website.') + + arg_parser.add_argument('-f', '--folder', help="""area to act on, eg "Tackley" or "2025/202504".""") + + arg_parser.add_argument('-i', '--reindex', action='store_true', help="""force recreation of indexes".""") + + args = arg_parser.parse_args() + + if args.command_str[0].upper() == 'COMPRESS': + source_bucket = 'ukmda-shared' + if args.folder: + prefix_str = 'archive/' + prefix_str + prefix_str = args.folder + if prefix_str[-1] != '/': + prefix_str = prefix_str + '/' + print(f'compressing {prefix_str}') + compressObjects(source_bucket, prefix_str) + + if args.command_str[0].upper() == 'PRUNE': + source_bucket = 'ukmda-website' + if args.folder: + prefix_str = 'reports/' + args.folder + if prefix_str[-1] != '/': + prefix_str = prefix_str + '/' + force_reindex = False + if args.reindex: + force_reindex = True + print(f'pruning {prefix_str} and forcing reindex') + pruneObjects(source_bucket, prefix_str, force_reindex) diff --git a/archive/ukmon_pylib/maintenance/getUserAndKeyInfo.py b/archive/ukmon_pylib/maintenance/getUserAndKeyInfo.py index 62af22c17..f1b477283 100644 --- a/archive/ukmon_pylib/maintenance/getUserAndKeyInfo.py +++ b/archive/ukmon_pylib/maintenance/getUserAndKeyInfo.py @@ -111,12 +111,17 @@ def getKeyStatuses(excludeadm=False): except ClientError as error: err = error.response['Error']['Code'] if err == 'CredentialReportNotPresentException' or err == 'CredentialReportExpiredException': + print('creating credentials report') res = iamc.generate_credential_report() + time.sleep(60) elif err == 'CredentialReportNotReadyException': + print('report not ready, sleeping 30s') time.sleep(30) gotit = False while not gotit: try: + res = iamc.generate_credential_report() + time.sleep(60) res = iamc.get_credential_report() gotit = True print('got the report') @@ -342,7 +347,10 @@ def rollKeysForUsers(nusers=3): _, stalekeys, _, _ = getKeyStatuses(excludeadm=True) if len(stalekeys) == 0: return - sampledf = stalekeys.sample(n=nusers) + if len(stalekeys) > nusers: + sampledf = stalekeys.sample(n=nusers) + else: + sampledf = stalekeys for _, rw in sampledf.iterrows(): uid = rw.user if 'testtraj' in uid: diff --git a/archive/ukmon_pylib/maintenance/manageTraj.py b/archive/ukmon_pylib/maintenance/manageTraj.py index dcb515298..fae24d735 100644 --- a/archive/ukmon_pylib/maintenance/manageTraj.py +++ b/archive/ukmon_pylib/maintenance/manageTraj.py @@ -7,7 +7,7 @@ # delete an orbit from the database -def deleteDuplicate(trajname): +def deleteDuplicate(trajname, jd=None): datadir = os.getenv('DATADIR', default='/home/pi/prod/data') yr=trajname[:4] if int(yr) > 2021: @@ -21,18 +21,26 @@ def deleteDuplicate(trajname): else: print("can only be done for 2022 onwards") return 0 - idx = df[df.orbname==trajname].index - if len(idx) > 0: + seldf = df[df.orbname==trajname] + idx = seldf.index + if jd: + idx = seldf[seldf._mjd == jd].index + if len(idx) == 1: df = df.drop(index=idx) df.to_parquet(fname, compression='snappy') csvfname = os.path.join(datadir, 'matched','matches-full-{}.csv'.format(yr)) df.to_csv(csvfname, index=False) - - deleteWebPage(trajname) + if jd is None: + deleteWebPage(trajname) + return 0 + elif len(idx) > 1: + print(f'two or more matches to {trajname}, please select MJD') + for _,rw in seldf.iterrows(): + print(f'{rw._mjd}, {rw.orbname}, {rw._amag}, {rw._stream}') return 1 else: print(f'no match for {trajname}') - return 0 + return 1 def deleteWebPage(trajname): diff --git a/archive/ukmon_pylib/maintenance/purgeCsvs.py b/archive/ukmon_pylib/maintenance/purgeCsvs.py new file mode 100644 index 000000000..e4a89feec --- /dev/null +++ b/archive/ukmon_pylib/maintenance/purgeCsvs.py @@ -0,0 +1,39 @@ +# copyright 2023- Mark McIntyre +# all rights reserved + +import boto3 +import sys + + +def findCsvsToDelete(archbucket, pref): + s3 = boto3.resource('s3') + s3c = boto3.client('s3') + bucket = s3.Bucket(archbucket) + patt = f'matches/single/rawcsvs/{pref}' + print(f'clearing down empty csvs from {archbucket}/{patt}... ', end='') + files = [obj.key for obj in bucket.objects.filter(Prefix=patt)] + filestodel = [] + for fil in files: + response = s3c.head_object(Bucket=archbucket, Key=fil) + size = response['ContentLength'] + if size < 100: + filestodel.append(fil) + numimgs = len(filestodel) + deleteZeroSizeFiles(filestodel, archbucket) + print(f'deleted {numimgs} empty CSVs') + return numimgs + + +def deleteZeroSizeFiles(flist, archbucket): + s3 = boto3.client('s3') + chunk_size = 900 + chunked_list = [flist[i:i + chunk_size] for i in range(0, len(flist), chunk_size)] + for ch in chunked_list: + delete_keys = {'Objects': []} + delete_keys['Objects'] = [{'Key': k} for k in ch] + s3.delete_objects(Bucket=archbucket, Delete=delete_keys) + return + + +if __name__ == '__main__': + findCsvsToDelete('ukmda-shared', sys.argv[1]) diff --git a/archive/ukmon_pylib/maintenance/recreateOrbitPages.py b/archive/ukmon_pylib/maintenance/recreateOrbitPages.py index 0e7f99532..4f5f050cf 100644 --- a/archive/ukmon_pylib/maintenance/recreateOrbitPages.py +++ b/archive/ukmon_pylib/maintenance/recreateOrbitPages.py @@ -40,7 +40,9 @@ def getImgList(outdir, traj): imglist = os.listdir(os.path.join(outdir, '..', 'jpgs')) return imglist orbname=traj.output_dir.replace('\\','/').split('/')[-1] - testdt = datetime.datetime.strptime(orbname.replace('-','_')[:15], '%Y%m%d_%h%m%s') + if orbname == '.': + orbname = os.path.split(outdir)[1] + testdt = datetime.datetime.strptime(orbname.replace('-','_')[:15], '%Y%m%d_%H%M%S') testdt = testdt + datetime.timedelta(seconds=-10) dtstr = testdt.strftime('%Y-%m-%dT%H:%M:%S.000Z') dtstr2 = (testdt + datetime.timedelta(seconds=30)).strftime('%Y-%m-%dT%H:%M:%S.000Z') @@ -134,6 +136,26 @@ def fixupTrajComments(traj, availableimages, outdir, picklename): return +def checkIfFileNeeded(filename): + + # update this if there's an additional file created by WMPL that we want + # to use on the website + # NB THIS ALSO HAS TO BE CHANGED in TRAJSOLVER if more files needed + if 'orbit_top.png' in filename or 'orbit_side.png' in filename or 'ground_track.png' in filename: + return True + if 'velocities.png' in filename or 'lengths.png' in filename or 'lags_all.png' in filename: + return True + if 'abs_mag.png' in filename or 'abs_mag_ht.png' in filename or 'report.txt' in filename: + return True + if 'all_angular_residuals.png' in filename or 'all_spatial_total_residuals_height.png' in filename: + return True + if 'trajectory.pickle' in filename: + return True + if 'extra' in filename: + return True + return False + + def recreateOrbitFiles(outdir, pickname, doupload=False): traj = loadPickle(outdir, pickname) traj.save_results = True @@ -147,8 +169,12 @@ def recreateOrbitFiles(outdir, pickname, doupload=False): print('created additional output') basename = pickname[:15] repname = basename + '_report.txt' - traj.saveReport(outdir, repname, None, False) - traj.savePlots(outdir, basename, show_plots=False, ret_figs=False) + try: + traj.saveReport(outdir, repname, None, False) + traj.savePlots(outdir, basename, show_plots=False, ret_figs=False) + except: + print('unable to process pickle properly') + pass print('created reports and figures') orbparent, orbfldr = os.path.split(outdir) yr = orbfldr[:4] @@ -159,7 +185,12 @@ def recreateOrbitFiles(outdir, pickname, doupload=False): availableimages = getImgList(outdir, traj) createExtraJpgtxt(outdir, traj, availableimages) createExtraJpgHtml(outdir, orbparent, yr, ym) - fixupTrajComments(traj, availableimages, outdir, pickname) + try: + fixupTrajComments(traj, availableimages, outdir, pickname) + except: + print('unable to fixup traj comments') + pass + if doupload: files = os.listdir(outdir) @@ -170,6 +201,9 @@ def recreateOrbitFiles(outdir, pickname, doupload=False): else: webfldr = f'reports/{yr}/orbits/{ym}' for fil in files: + if not checkIfFileNeeded(fil): + print(f'skipping {fil}') + continue locfname = f'{outdir}/{fil}' if 'summary' in fil or 'extrajpgs.txt' or 'html' in fil: keyname = f"{webfldr}/{orbfldr}/{fil}" diff --git a/archive/ukmon_pylib/maintenance/rerunFailedLambdas.py b/archive/ukmon_pylib/maintenance/rerunFailedLambdas.py index 186be1274..feb2a5079 100644 --- a/archive/ukmon_pylib/maintenance/rerunFailedLambdas.py +++ b/archive/ukmon_pylib/maintenance/rerunFailedLambdas.py @@ -83,17 +83,17 @@ def checkFails(fails): s3bucket=os.getenv('WEBSITEBUCKET', default='s3://ukmda-website')[5:] realfails = [] for f in fails: - # example f : matches/RMSCorrelate/trajectories/2022/202202/20220227/20220227_011240.522_UK + # example f : matches/RMSCorrelate/trajectories/2022/202202/20220227/20220227_011240.522_UK/20220227_011240_trajectory.pickle orb = f[f.find('/20')+1:] + orb = 'reports/' + orb[:5] + 'orbits' + orb[4:] pth, _ = os.path.split(orb) - _, zipname = os.path.split(pth) - keyf = pth + '/' + zipname + '.zip' + keyf = f'{pth}/index.html' try: _ = s3.head_object(Bucket=s3bucket, Key=keyf) #print(f'{pth} already done') except: - print(f'adding {keyf}') + print(f'adding {f}') realfails.append(f) return realfails diff --git a/archive/ukmon_pylib/metrics/getMatchStats.py b/archive/ukmon_pylib/metrics/getMatchStats.py index da0c3c714..8114c4e3a 100644 --- a/archive/ukmon_pylib/metrics/getMatchStats.py +++ b/archive/ukmon_pylib/metrics/getMatchStats.py @@ -7,6 +7,32 @@ import sys +def getDailyObsCounts(logf): + loglines = open(logf).readlines() + addlines = [line.strip() for line in loglines if ('Added' in line and 'observations' in line) or 'Processing station' in line] + res=[0,0,0,0] + stn = '' + offs = 0 + totalval = 0 + uniquestns = 0 + for i in range(len(addlines)): + if 'Processing' in addlines[i]: + print(addlines[i]) + newstn = addlines[i].split(' ')[-1] + if stn != newstn: + stn = newstn + offs = 0 + uniquestns += 1 + if 'Processing' in addlines[i+1]: + continue + val = int(addlines[i+1].split(' ')[1]) + totalval += val + res[offs] += val + offs += 1 + print(res, uniquestns) + return + + def getMatchStats(logf): with open(logf) as inf: loglines = inf.readlines() diff --git a/archive/ukmon_pylib/reports/CameraDetails.py b/archive/ukmon_pylib/reports/CameraDetails.py index 49e5680ec..74ce7ac86 100644 --- a/archive/ukmon_pylib/reports/CameraDetails.py +++ b/archive/ukmon_pylib/reports/CameraDetails.py @@ -126,7 +126,7 @@ def createStatOptsHtml(sitedets, datadir, active=False, locs=False): def findSite(stationid, camdets=None, ddb=None): if camdets is None: camdets = loadLocationDetails(ddb=ddb) - cc = camdets[camdets.stationid==stationid] + cc = camdets[camdets.stationid==stationid[:6]] if len(cc) > 0: res = cc.iloc[0]['site'] else: @@ -137,7 +137,7 @@ def findSite(stationid, camdets=None, ddb=None): def findEmail(stationid, camdets=None, ddb=None): if camdets is None: camdets = loadLocationDetails(ddb=ddb) - cc = camdets[camdets.stationid==stationid] + cc = camdets[camdets.stationid==stationid[:6]] if len(cc) > 0: res = cc.iloc[0]['eMail'] else: diff --git a/archive/ukmon_pylib/reports/cameraStatusReport.py b/archive/ukmon_pylib/reports/cameraStatusReport.py index 328a910c3..0872feff7 100644 --- a/archive/ukmon_pylib/reports/cameraStatusReport.py +++ b/archive/ukmon_pylib/reports/cameraStatusReport.py @@ -9,7 +9,7 @@ import pandas as pd -def getLastUpdateDate(datadir=None, camfname=None): +def getLastUpdateDate(datadir=None, camfname=None, ddb=None): """ Create a status report showing the last update date of each camera that is providing data @@ -19,7 +19,7 @@ def getLastUpdateDate(datadir=None, camfname=None): includenever (bool) default false, include cameras that have never uploaded """ - caminfo = loadLocationDetails() + caminfo = loadLocationDetails(ddb=ddb) caminfo = caminfo[caminfo.active==1] caminfo = caminfo.drop(columns=['direction','oldcode','active','camtype','eMail', 'humanName']) diff --git a/archive/ukmon_pylib/reports/createSearchableFormat.py b/archive/ukmon_pylib/reports/createSearchableFormat.py index 2e47d56f7..e2df79c2b 100644 --- a/archive/ukmon_pylib/reports/createSearchableFormat.py +++ b/archive/ukmon_pylib/reports/createSearchableFormat.py @@ -9,10 +9,27 @@ import os import pandas as pd import datetime +import boto3 + + +def checkUrl(s3, url): + siteroot = os.getenv('WEBSITEBUCKET', default='s3://ukmda-website') + retval = url + tmpurl = url + if url[0]==',': + tmpurl = url[1:] + try: + _ = s3.head_object(Bucket=siteroot[5:], Key=tmpurl[1:]) + except Exception: + #print(e) + retval = '/img/missing-white.png' + print(url, retval) + return retval def convertSingletoSrchable(datadir, year, newonly=True): print(datetime.datetime.now(), 'single-detection searchable index start') + s3 = boto3.client('s3') # load the single-station combined data if newonly is False: @@ -34,6 +51,10 @@ def convertSingletoSrchable(datadir, year, newonly=True): uadata['fn']=[f'/img/single/{y}/{y}{m:02d}/'+f.replace('.fits','.jpg') for f,y,m in zip(uadata.Filename, uadata.Y, uadata.M)] + print(datetime.datetime.now(), 'checking target urls exist') + uadata['targfn'] = [checkUrl(s3, x) for x in uadata.fn] + print(datetime.datetime.now(), 'done') + # create array for source print(datetime.datetime.now(), 'add source column') srcs = ['2Single']*len(uadata.Filename) @@ -44,41 +65,13 @@ def convertSingletoSrchable(datadir, year, newonly=True): print(datetime.datetime.now(), 'create interim dataframe') hdr=['eventtime','source','shower','Mag','loccam','url','imgs', 'loctime', 'Y','M'] resdf = pd.DataFrame(zip(uadata.Dtstamp, srcs, uadata.Shwr, - uadata.Mag, uadata.ID, uadata.fn, uadata.fn, uadata.LocalTime, + uadata.Mag, uadata.ID, uadata.targfn, uadata.targfn, uadata.LocalTime, uadata.Y, uadata.M), columns=hdr) # fix up some mangled historical data resdf.loc[resdf.loccam=='Ringwood_N_UK000S', 'loccam'] = 'UK000S' resdf.loc[resdf.loccam=='Tackley_SW_UK0006', 'loccam'] = 'UK0006' - # select the RMS data out, its good now - # FIXME - needs to select for "not FF_UK9" so we can include non-UK cameras - rmsdata=resdf[resdf.url.str.contains('FF_UK0')] - rmsdata = rmsdata.drop(columns=['Y','M','loctime']) - - # now select out the UFO dta and fix it up - ufodata=resdf[resdf.url.str.contains('FF_UK9')] - #fix up Clanfield cameras - ufodata.loc[ufodata.loccam=='UK9990', 'loccam'] = 'Clanfield_NE' - ufodata.loc[ufodata.loccam=='UK9989', 'loccam'] = 'Clanfield_NW' - ufodata.loc[ufodata.loccam=='UK9988', 'loccam'] = 'Clanfield_SE' - ufodata = ufodata.drop(columns=['url','imgs']) - - # create the URL and imgs fields - ufodata['url']=[f'/img/single/{y}/{y}{m:02d}/M{lt}_{f}P.jpg' - for f,y,m,lt in zip(ufodata.loccam, ufodata.Y, ufodata.M, ufodata.loctime)] - ufodata['imgs'] = ufodata.url - ufodata = ufodata.drop(columns=['loctime','Y','M']) - - # annoying special case for UK0001, H and S which do not upload JPGs - rmsdata.loc[rmsdata.loccam=='UK0001','url']='/img/missing-white.png' - rmsdata.loc[rmsdata.loccam=='UK0001','imgs']='/img/missing-white.png' - rmsdata.loc[rmsdata.loccam=='UK000H','url']='/img/missing-white.png' - rmsdata.loc[rmsdata.loccam=='UK000H','imgs']='/img/missing-white.png' - rmsdata.loc[rmsdata.loccam=='UK000S','url']='/img/missing-white.png' - rmsdata.loc[rmsdata.loccam=='UK000S','imgs']='/img/missing-white.png' - - resdf = pd.concat([rmsdata,ufodata]) if newonly is True: return resdf, rmsuafile else: @@ -114,19 +107,22 @@ def convertMatchToSrchable(datadir, year, newonly=True): if __name__ == '__main__': if len(sys.argv) < 3: - print('usage: python createSearchableFormat.py year dest mode') + print('usage: python createSearchableFormat.py year mode outdir') exit(1) else: datadir = os.getenv('DATADIR', default='/home/ec2-user/prod/data') year = sys.argv[1] mode = sys.argv[2] + outdir = os.path.join(datadir, 'searchidx') + if len(sys.argv) > 3: + outdir = sys.argv[3] # create a set of single-station data and merge with last match set if mode == 'singles': print(datetime.datetime.now(), 'converting single-station data') newsingles, fname = convertSingletoSrchable(datadir, year, True) - outfile = os.path.join(datadir, 'searchidx', '{:s}-singles-new.csv'.format(year)) + outfile = os.path.join(outdir, '{:s}-singles-new.csv'.format(year)) if newsingles is not None: newsingles.to_csv(outfile, index=False, header=False) if fname is not None: @@ -136,11 +132,11 @@ def convertMatchToSrchable(datadir, year, newonly=True): elif mode == 'matches': print(datetime.datetime.now(), 'converting match data') newmatches, fname = convertMatchToSrchable(datadir, year, True) - outfile = os.path.join(datadir, 'searchidx', '{:s}-matches-new.csv'.format(year)) + outfile = os.path.join(outdir, '{:s}-matches-new.csv'.format(year)) if newmatches is not None: newmatches.to_csv(outfile, index=False, header=False) if fname is not None: os.remove(fname) else: - print('usage: createSearchableFormat yyyy matches_or_singles') + print('usage: createSearchableFormat year mode outdir') diff --git a/archive/ukmon_pylib/reports/findBestMp4s.py b/archive/ukmon_pylib/reports/findBestMp4s.py index bd0af136c..a07611ddd 100644 --- a/archive/ukmon_pylib/reports/findBestMp4s.py +++ b/archive/ukmon_pylib/reports/findBestMp4s.py @@ -5,9 +5,81 @@ import shutil import sys import os +import datetime +import requests from traj.pickleAnalyser import getAllMp4s +def getBestNMatches(reqdate=None, numtoget=10): + if reqdate is None: + tod = datetime.datetime.now() + tod = tod.replace(hour=12, minute=0, second=0, microsecond=0) + reqdate = tod + datetime.timedelta(days=-1) + else: + reqdate = reqdate.replace(hour=12, minute=0, second=0, microsecond=0) + tod = reqdate + datetime.timedelta(days=1) + + yr = reqdate.year + datadir=os.getenv('DATADIR', default='/home/ec2-user/prod/data') + mf = os.path.join(datadir, 'matched', f'matches-full-{yr}.parquet.snap') + + # select only the columns we need + cols=['_localtime', '_mag','url'] + matches = pd.read_parquet(mf, columns=cols) + matches['dt'] = [datetime.datetime.strptime(x,'_%Y%m%d_%H%M%S') for x in matches._localtime] + + matches = matches[matches.dt >= reqdate] + matches = matches[matches.dt <= tod] + sepdata = matches.sort_values(by=['_mag']) + sorteddata = sepdata.head(numtoget) + sorteddata.drop_columns(['_localtime'], inplace=True) + + +def getBestNSingles(reqdate=None, numtoget=20, shwr=None, outdir=None): + if reqdate is None: + tod = datetime.datetime.now() + tod = tod.replace(hour=12, minute=0, second=0, microsecond=0) + reqdate = tod + datetime.timedelta(days=-1) + else: + print(reqdate) + reqdate = datetime.datetime.strptime(reqdate, '%Y%m%d') + reqdate = reqdate.replace(hour=12, minute=0, second=0, microsecond=0) + tod = reqdate + datetime.timedelta(days=1) + yr = reqdate.year + url = f'https://archive.ukmeteors.co.uk/browse/parquet/singles-{yr}.parquet.snap' + + # select only the columns we need + cols=['Mag','Shwr','Filename','Dtstamp'] + matches = pd.read_parquet(url, columns=cols) + + matches = matches[matches.Dtstamp >= reqdate.timestamp()] + matches = matches[matches.Dtstamp <= tod.timestamp()] + sepdata = matches.sort_values(by=['Mag']) + sepdata['url'] = [getUrlFromFilename(x) for x in sepdata.Filename] + if shwr: + sepdata = sepdata[sepdata.Shwr==shwr] + sorteddata = sepdata.head(numtoget) + if outdir: + outdir = os.path.join(outdir, reqdate.strftime('%Y%m%d')) + os.makedirs(outdir, exist_ok=True) + for _, rw in sorteddata.iterrows(): + fname = (f'{rw.Shwr}_{rw.Mag}_{rw.Filename}').replace('.fits','.jpg') + print(fname) + res = requests.get(rw.url) + if res.status_code == 200: + open(os.path.join(outdir, fname),'wb').write(res.content) + + filtereddata = sorteddata.drop(columns=['Dtstamp','Filename']) + return filtereddata + + +def getUrlFromFilename(fname): + ymd = fname.split('_')[2] + jpgname = fname.replace('.fits','.jpg') + url = f'https://archive.ukmeteors.co.uk/img/single/{ymd[:4]}/{ymd[:6]}/{jpgname}' + return url + + def getBestNMp4s(yr, mth, numtoget): datadir=os.getenv('DATADIR', default='/home/ec2-user/prod/data') mf = os.path.join(datadir, 'matched', f'matches-full-{yr}.parquet.snap') diff --git a/archive/ukmon_pylib/reports/findFireballs.py b/archive/ukmon_pylib/reports/findFireballs.py index 49575426b..60b7af0c0 100644 --- a/archive/ukmon_pylib/reports/findFireballs.py +++ b/archive/ukmon_pylib/reports/findFireballs.py @@ -67,8 +67,16 @@ def createMDFiles(fbs, outdir, orbdir): targfile = os.path.join(tmpdir, 'jpgs.lst') try: s3.download_file(srcbucket, fname, targfile) - bestimg = getBestView(pickfile) - #print(bestimg) + bestimg = getBestView(pickfile) + if bestimg[0] == '[': + bestimg = bestimg.replace('[','').replace(']','') + bestimg = bestimg.split(',') + bestimg = bestimg[0] + if bestimg[0] == "'": + bestimg = bestimg[1:-1] + if type(bestimg) is list: + bestimg = bestimg[0] + print(f' bestimg is "{bestimg}"') if bestimg[:3] == 'FF_': pth = fb.url[:fb.url.find('reports/')] bestimg = f'img/single/{yr}/{ym}/{bestimg}' @@ -83,8 +91,7 @@ def createMDFiles(fbs, outdir, orbdir): bestimgurl = '' except: print('unable to collect jpgs.lst') - - fname = loctime.strftime('%Y%m%d_%H%M%S') + '.md' + fname = f'{trajdir[:15]}.md' if os.path.isfile(os.path.join(outdir,fname)): print(f'{fname} exists, not replacing it') else: diff --git a/archive/ukmon_pylib/reports/meteoriteTools.py b/archive/ukmon_pylib/reports/meteoriteTools.py new file mode 100644 index 000000000..4f7c72e7b --- /dev/null +++ b/archive/ukmon_pylib/reports/meteoriteTools.py @@ -0,0 +1,55 @@ +# Find cameras near to a point and other tools relevant to meteorite finds + +# Copyright (C) 2018-2023 Mark McIntyre + +import sys +import json +import pandas as pd +import numpy as np +import requests + +from meteortools.utils.Math import greatCircleDistance + + +def stationsNearPoint(lat, lon, dist=75, email_only=True): + lat = np.radians(lat) + lon = np.radians(lon) + res = requests.get('https://archive.ukmeteors.co.uk/browse/cameraLocs.json') + if res.status_code != 200: + return None + jsdata = json.loads(res.text) + camlocs = pd.DataFrame(jsdata).transpose() + camlocs.drop(columns=['ele','az','alt','fov_h','fov_v','rot'], inplace=True) + camlocs['dist'] = [greatCircleDistance(np.radians(statlat), np.radians(statlon), lat, lon) + for statlat,statlon in zip(camlocs.lat, camlocs.lon)] + nearby = camlocs[camlocs.dist < dist] + cams = {} + for cam in nearby.index: + res = requests.get(f'https://api.ukmeteors.co.uk/camdetails?camid={cam}') + if res.status_code == 200: + dta = json.loads(res.text) + if len(dta) > 0: + cams[cam] = {'email': dta[0]['eMail']} + else: + cams[cam] = {'email':'unknown'} + else: + cams[cam] = {'email':'unknown'} + camlist = nearby.merge(pd.DataFrame(cams).transpose(), how='left', left_index=True, right_index=True) + if email_only: + camlist.drop(columns=['lat','lon','dist'], inplace=True) + camlist.drop_duplicates(inplace=True) + return camlist + + +def getCoordsFromTraj(trajname): + lat = 0 + lon = 0 + return lat, lon + + +if __name__ == '__main__': + lat = float(sys.argv[1]) + lon = float(sys.argv[2]) + dist = float(sys.argv[3]) + + print(stationsNearPoint(lat, lon, dist)) diff --git a/archive/ukmon_pylib/tests/test_reports_CameraDetails.py b/archive/ukmon_pylib/tests/test_reports_CameraDetails.py index 607bd6cfd..2e57dbd19 100644 --- a/archive/ukmon_pylib/tests/test_reports_CameraDetails.py +++ b/archive/ukmon_pylib/tests/test_reports_CameraDetails.py @@ -3,6 +3,7 @@ import datetime import os import shutil +import boto3 from reports.CameraDetails import getCamLocDirFov, updateCamLocDirFovDB from reports.CameraDetails import loadLocationDetails, findEmail, findSite @@ -25,7 +26,11 @@ def test_updateCamLocDirFovDB(): def test_loadLocationDetails(): - caminfo = loadLocationDetails() + # for testing we should precreate the DDB connection using the default role + conn = boto3.Session() + ddb = conn.resource('dynamodb', region_name='eu-west-2') + + caminfo = loadLocationDetails(ddb=ddb) caminfo = caminfo[caminfo.stationid=='UK0006'] assert len(caminfo) == 1 diff --git a/archive/ukmon_pylib/tests/test_reports_cameraStatusReport.py b/archive/ukmon_pylib/tests/test_reports_cameraStatusReport.py index c67ec74b9..668f37318 100644 --- a/archive/ukmon_pylib/tests/test_reports_cameraStatusReport.py +++ b/archive/ukmon_pylib/tests/test_reports_cameraStatusReport.py @@ -3,6 +3,7 @@ import datetime import os +import boto3 from reports.cameraStatusReport import getLastUpdateDate, createStatusReportJSfile @@ -13,7 +14,11 @@ def test_createStatusReportJSfile(): - stati = getLastUpdateDate(datadir) + # for testing we should precreate the DDB connection using the default role + conn = boto3.Session() + ddb = conn.resource('dynamodb', region_name='eu-west-2') + + stati = getLastUpdateDate(datadir=datadir, ddb=ddb) assert 'UK0006' in list(stati.stationid) createStatusReportJSfile(stati, datadir) csvf = os.path.join(datadir, 'reports', 'camrep.js') diff --git a/archive/ukmon_pylib/traj/README.md b/archive/ukmon_pylib/traj/README.md index 8b6280a2e..7cb699d70 100644 --- a/archive/ukmon_pylib/traj/README.md +++ b/archive/ukmon_pylib/traj/README.md @@ -12,7 +12,7 @@ routines associated with trajectory solving and analysis of results * plotOSMGroundTrack.py - creates an OS-style ground map from a trajectory pickle. * showerAssociation.py - creates shower association information from a trajectory pickle. -* clusdetails-ee.txt - files autogenerated by Terraform containing cluster details for the distributed solver +* clusdetails-mda.txt - files autogenerated by Terraform containing cluster details for the solver * clusdetails-mm.txt * clusdetails.txt diff --git a/archive/ukmon_pylib/traj/createDistribMatchingSh.py b/archive/ukmon_pylib/traj/createDistribMatchingSh.py index 2705bdff1..97555ca96 100644 --- a/archive/ukmon_pylib/traj/createDistribMatchingSh.py +++ b/archive/ukmon_pylib/traj/createDistribMatchingSh.py @@ -135,7 +135,7 @@ def createExecConsolSh(matchstart, matchend, execconsolsh): outf.write('#!/bin/bash\n') outf.write('source /home/ec2-user/venvs/wmpl/bin/activate\n') outf.write('export PYTHONPATH=/home/ec2-user/src/WesternMeteorPyLib:/home/ec2-user/src/ukmon_pylib\n') - outf.write('export AWS_PROFILE=ukmonshared\n') + # outf.write('export AWS_PROFILE=ukmonshared\n') outf.write(f'cd {calcdir}\n') outf.write('logger -s -t execConsol start\n') @@ -150,7 +150,7 @@ def createExecConsolSh(matchstart, matchend, execconsolsh): pushUpdatedTrajectoriesWeb(outf, matchstart, matchend, webbucket) outf.write('logger -s -t execConsol getting the image list\n') gatherUsedImageList(outf, matchstart, matchend, shbucket) - outf.write('unset AWS_PROFILE\n') + #outf.write('unset AWS_PROFILE\n') outf.write('logger -s -t execConsol done\n') return @@ -164,13 +164,13 @@ def createExecReplotSh(matchstart, matchend, execconsolsh): outf.write('#!/bin/bash\n') outf.write('source /home/ec2-user/venvs/wmpl/bin/activate\n') outf.write('export PYTHONPATH=/home/ec2-user/src/WesternMeteorPyLib:/home/ec2-user/src/ukmon_pylib\n') - outf.write('export AWS_PROFILE=ukmonshared\n') + #outf.write('export AWS_PROFILE=ukmonshared\n') outf.write(f'cd {calcdir}\n') outf.write('logger -s -t execReplot start\n') refreshTrajectories(outf, matchstart, matchend, outpath) createDensityPlots(outf, calcdir, enddt, False) gatherUsedImageList(outf, matchstart, matchend, shbucket) - outf.write('unset AWS_PROFILE\n') + #outf.write('unset AWS_PROFILE\n') outf.write('logger -s -t execReplot done\n') return @@ -194,7 +194,7 @@ def createDistribMatchingSh(matchstart, matchend, execmatchingsh): outf.write('#!/bin/bash\n') outf.write('source /home/ec2-user/venvs/wmpl/bin/activate\n') outf.write('export PYTHONPATH=/home/ec2-user/src/WesternMeteorPyLib:/home/ec2-user/src/ukmon_pylib\n') - outf.write('export AWS_PROFILE=ukmonshared\n') + #outf.write('export AWS_PROFILE=ukmonshared\n') outf.write(f'cd {calcdir}\n') outf.write('df -h . \n') @@ -236,7 +236,7 @@ def createDistribMatchingSh(matchstart, matchend, execmatchingsh): pushUpdatedTrajectoriesShared(outf, matchstart, matchend, shbucket) pushUpdatedTrajectoriesWeb(outf, matchstart, matchend, webbucket) - outf.write('unset AWS_PROFILE\n') + #outf.write('unset AWS_PROFILE\n') outf.write('logger -s -t execdistrib done\n') diff --git a/archive/ukmon_pylib/traj/distributeCandidates.py b/archive/ukmon_pylib/traj/distributeCandidates.py index 1c0254082..3030c9b08 100644 --- a/archive/ukmon_pylib/traj/distributeCandidates.py +++ b/archive/ukmon_pylib/traj/distributeCandidates.py @@ -19,9 +19,7 @@ def getClusterDetails(templdir): sts = boto3.client('sts') accid = sts.get_caller_identity()['Account'] - if accid == '822069317839': - clusdetails = os.path.join(templdir, 'clusdetails-ee.txt') - elif accid == '183798037734': + if accid == '183798037734': clusdetails = os.path.join(templdir, 'clusdetails-mda.txt') else: clusdetails = os.path.join(templdir, 'clusdetails-mm.txt') diff --git a/usermgmt/addAdminUser.ps1 b/archive/usermgmt/addAdminUser.sh similarity index 96% rename from usermgmt/addAdminUser.ps1 rename to archive/usermgmt/addAdminUser.sh index 3158fb995..eb02ec10c 100644 --- a/usermgmt/addAdminUser.ps1 +++ b/archive/usermgmt/addAdminUser.sh @@ -1,5 +1,6 @@ -# simple script to create a new user-maintenance account -# and save the access key locally. Can only be used by an account with IAM Admin permissions. -$username = $args[0] -aws iam create-user --user-name $username --profile ukmda_admin -aws iam add-user-to-group --user-name $username --user-group Administrators --profile ukmda_admin +#!/bin/bash +# simple script to create a new user-maintenance account +# and save the access key locally. Can only be used by an account with IAM Admin permissions. +$username = $args[0] +aws iam create-user --user-name $username --profile ukmda_admin +aws iam add-user-to-group --user-name $username --user-group Administrators --profile ukmda_admin diff --git a/usermgmt/addFBApiKey.ps1 b/archive/usermgmt/addFBApiKey.sh similarity index 58% rename from usermgmt/addFBApiKey.ps1 rename to archive/usermgmt/addFBApiKey.sh index c903c2eb4..eb957f893 100644 --- a/usermgmt/addFBApiKey.ps1 +++ b/archive/usermgmt/addFBApiKey.sh @@ -1,6 +1,7 @@ -# copyright Mark McIntyre, 2024- - -# script to add an api key for a user - -$username = $args[0] -aws apigateway create-api-key --name "$username" --region eu-west-1 --profile ukmda_admin --description "key for $username" --enabled \ No newline at end of file +#!/bin/bash +# copyright Mark McIntyre, 2024- + +# script to add an api key for a user + +$username = $1 +aws apigateway create-api-key --name "$username" --region eu-west-1 --description "key for $username" --enabled --profile ukmda_admin \ No newline at end of file diff --git a/usermgmt/server/addSftpUser.sh b/archive/usermgmt/addSftpUser.sh similarity index 93% rename from usermgmt/server/addSftpUser.sh rename to archive/usermgmt/addSftpUser.sh index 8ddb730cd..82fdcbd26 100644 --- a/usermgmt/server/addSftpUser.sh +++ b/archive/usermgmt/addSftpUser.sh @@ -44,7 +44,7 @@ if [ ! -f $keydir/keys/$shortid_l.key ] ; then fi # add a unix user and set their homedir to /var/sftp/userid -grep $userid /etc/passwd +grep -w $userid /etc/passwd if [ $? -eq 1 ] ; then dt=$(date +%Y-%m-%d) logger -s -t addSftpUser "Creating unix user $userid" @@ -91,6 +91,10 @@ if [ ! -z $oldloc ] ; then logger -s -t addSftpUser "Moving $oldloc to $userid" sudo cp /var/sftp/$oldloc/ukmon.ini /var/sftp/$oldloc/ukmon.ini.bkp sudo cp $keydir/inifs/$userid.ini /var/sftp/$oldloc/ukmon.ini + # and copy the ssh authorized keys file + sudo cp $oldloc/.ssh/authorized_keys /var/sftp/$userid/.ssh/ + sudo chown -R $userid:$userid /var/sftp/$userid/.ssh/authorized_keys + sudo chmod 644 /var/sftp/$userid/.ssh/authorized_keys fi logger -s -t addSftpUser "Finished" diff --git a/usermgmt/windows/camTableMaintenance.py b/archive/usermgmt/camTableMaintenance.py similarity index 100% rename from usermgmt/windows/camTableMaintenance.py rename to archive/usermgmt/camTableMaintenance.py diff --git a/usermgmt/windows/testkeys.py b/archive/usermgmt/testkeys.py similarity index 100% rename from usermgmt/windows/testkeys.py rename to archive/usermgmt/testkeys.py diff --git a/usermgmt/server/updateAwsKey.sh b/archive/usermgmt/updateAwsKey.sh similarity index 68% rename from usermgmt/server/updateAwsKey.sh rename to archive/usermgmt/updateAwsKey.sh index ab53776a1..ff508560d 100644 --- a/usermgmt/server/updateAwsKey.sh +++ b/archive/usermgmt/updateAwsKey.sh @@ -8,7 +8,14 @@ targ=$1 force=$2 here="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" -source $here/../config.ini >/dev/null 2>&1 +if [ -f ../config.ini ] ; then + source ~/dev/config.ini + keydir=/home/ec2-user/dev/keymgmt +else + source ~/prod/config.ini + keydir=/home/ec2-user/keymgmt + +fi conda activate $HOME/miniconda3/envs/${WMPL_ENV} export AWS_PROFILE=ukmonshared diff --git a/usermgmt/server/updateKeysForSplit.py b/archive/usermgmt/updateKeysForSplit.py similarity index 100% rename from usermgmt/server/updateKeysForSplit.py rename to archive/usermgmt/updateKeysForSplit.py diff --git a/archive/utils/checkCalcServerRunTime.sh b/archive/utils/checkCalcServerRunTime.sh new file mode 100644 index 000000000..8380d69f3 --- /dev/null +++ b/archive/utils/checkCalcServerRunTime.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# script to extract the start/stop times of the Calculation Engine server from the logs +# I have a spreadsheet that consumes this and works out the cost for various models of server (c8g.2xlarge, c8g.4xlarge etc) + +cd ~/prod/logs + +csl=/tmp/calcserver_timings.txt +csl2=/tmp/csl.txt +[ -f $csl ] && rm $csl +touch $csl + +ls -1tr nightly*.gz | while read i +do + mfz=matchJob${i:10:50} + gunzip $i + gunzip $mfz + nf=$(basename $i .gz) + mf=$(basename $mfz .gz) + egrep "start correlation server|stop the server again|restarting server to|stopping calcserver again" $mf $nf >> $csl + gzip $nf + gzip $mf +done +ls -1tr nightlyJob* | grep -v gz | while read nf +do + mf=matchJob${nf:10:50} + egrep "start correlation server|stop the server again|restarting server to|stopping calcserver again" $mf $nf >> $csl +done +[ -f $csl2 ] && rm $csl2 +touch $csl2 +cat $csl | while read i +do + parta=$(echo $i | awk -F ">" '{print $2}') + partb=$(echo $parta | awk -F " " '{printf("%s, %s, %s\n",$1,$2,$3)}') + echo $partb >> $csl2 +done +[ -f $csl ] && rm $csl +python << EOD +lis = open('$csl2','r').readlines() +data = [x.strip().split(',') for x in lis] +currdt = None +results = [] +res = [] +col = 0 +with open('/tmp/computeservertimes.csv','w') as outf: + for i in range(len(data)): + mth = data[i][0] + dy = data[i][1] + tim = data[i][2] + dtstr = f'{mth} {dy}' + if currdt is None: + currdt = dtstr + if currdt != dtstr: + outf.write(','.join(res) + '\n') + res = [] + col = 0 + if col == 0: + res.append(dtstr) + res.append(tim) + else: + res.append(tim) + col += 1 + currdt = dtstr + outf.write(','.join(res) + '\n') +EOD +[ -f $csl2 ] && rm $csl2 diff --git a/archive/utils/createTestDataSet.sh b/archive/utils/createTestDataSet.sh index 66601bc74..499bf238d 100644 --- a/archive/utils/createTestDataSet.sh +++ b/archive/utils/createTestDataSet.sh @@ -4,14 +4,30 @@ here="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" source $here/../config.ini >/dev/null 2>&1 -YYMMDD=20220120 +if [ "$1" == "" ] ; then + echo bad input + exit 0 +fi +dt=$1 +numdays=$2 basepth=~/dev/data/RMSCorrelate -mkdir $basepth +mkdir -p $basepth -fldrs=$(aws s3 ls s3://ukmda-shared/matches/RMSCorrelate/ | egrep "UK|BE|IE" | awk '{print $2}') +fldrs=$(aws s3 ls s3://ukmda-shared/matches/RMSCorrelate/ | egrep "UK|BE|IE|NL" | awk '{print $2}') for fldr in $fldrs ; do - camid=${fldr:0:6} - aws s3 sync s3://ukmda-shared/matches/RMSCorrelate/${fldr} ${basepth}/${fldr} --exclude "*" --include "${camid}_${YYMMDD}*" + for i in $(seq 1 $numdays) ; do + d2=$(python -c "import datetime;d1=datetime.datetime.strptime('$dt', '%Y%m%d')+datetime.timedelta(days=$i-1);print(d1.strftime('%Y%m%d'))") + camid=${fldr:0:6} + echo checking $camid for $d2 + aws s3 sync s3://ukmda-shared/matches/RMSCorrelate/${fldr} ${basepth}/${fldr} --exclude "*" --include "${camid}_${d2}*" + done done + +for i in $(seq 1 $numdays) ; do + d2=$(python -c "import datetime;d1=datetime.datetime.strptime('$dt', '%Y%m%d')+datetime.timedelta(days=$i-1);print(d1.strftime('%Y%m%d'))") + echo getting trajectories for $d2 + fldr="trajectories/${d2:0:4}/${d2:0:6}/$d2" + aws s3 sync s3://ukmda-shared/matches/RMSCorrelate/${fldr} ${basepth}/${fldr} --exclude "*" --include "${d2}*" +done diff --git a/archive/utils/deleteOrbit.sh b/archive/utils/deleteOrbit.sh index 71dbdf60d..0338b1fff 100644 --- a/archive/utils/deleteOrbit.sh +++ b/archive/utils/deleteOrbit.sh @@ -5,10 +5,19 @@ here="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" source $here/../config.ini >/dev/null 2>&1 conda activate $HOME/miniconda3/envs/${WMPL_ENV} +if [ "$1" == "" ] ; then + echo "usage: deleteOrbit.sh fulltrajname {optional jd}" + exit 0 +fi + traj=$1 ymd=${traj:0:8} -python -c "from maintenance.manageTraj import deleteDuplicate as dd; dd('$traj'); " +if [ "$2" == "" ]; then + python -c "from maintenance.manageTraj import deleteDuplicate as dd; dd('$traj'); " +else + python -c "from maintenance.manageTraj import deleteDuplicate as dd; dd('$traj', $2); " +fi if [ $? == 0 ] ; then $SRC/website/createOrbitIndex.sh $ymd diff --git a/archive/utils/getGmnData.sh b/archive/utils/getGmnData.sh new file mode 100644 index 000000000..bfbaf9398 --- /dev/null +++ b/archive/utils/getGmnData.sh @@ -0,0 +1,19 @@ +#!/bin/bash +here="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +source $here/../config.ini >/dev/null 2>&1 +conda activate $HOME/miniconda3/envs/${WMPL_ENV} + +cd $DATADIR/gmndata + +y1=$(date +%Y) +y2=$(date +%Y --date 'yesterday') +rsync -vtz gmn.uwo.ca:/srv/meteor-ro/rms/gmn/extracted_data/auto_output/traj_summary_data/*$y1.txt . --exclude traj_summary_all.txt +if [ "$ym1" != "$ym2" ] ; then + rsync -vtz gmn.uwo.ca:/srv/meteor-ro/rms/gmn/extracted_data/auto_output/traj_summary_data/*$y2.txt . --exclude traj_summary_all.txt +fi +ym1=$(date +%Y%m) +ym2=$(date +%Y%m --date 'yesterday') +rsync -vtz gmn.uwo.ca:/srv/meteor-ro/rms/gmn/extracted_data/auto_output/traj_summary_data/monthly/*${ym1}.txt ./monthly --exclude traj_summary_all.txt +if [ "$ym1" != "$ym2" ] ; then + rsync -vtz gmn.uwo.ca:/srv/meteor-ro/rms/gmn/extracted_data/auto_output/traj_summary_data/monthly/*${ym2}.txt ./monthly --exclude traj_summary_all.txt +fi \ No newline at end of file diff --git a/archive/utils/statsToMqtt.sh b/archive/utils/statsToMqtt.sh index 50b58a699..3235281a2 100644 --- a/archive/utils/statsToMqtt.sh +++ b/archive/utils/statsToMqtt.sh @@ -5,7 +5,7 @@ if [ "$(hostname)" == "wordpresssite" ] ; then source /home/bitnami/venvs/openhabstuff/bin/activate python $here/statsToMqtt.py diskpct=$(df / |tail -1| awk '{print $5 }' | sed 's/%//g') - if [ $diskpct -gt 90 ] ; then + if [ $diskpct -gt 9 ] ; then /home/bitnami/src/maintenance/mysql_checks.sh msg="$(hostname) diskspace was ${diskpct}%" subj="Diskspace alert for $(hostname)" @@ -27,7 +27,7 @@ else export PYTHONPATH=$SRC/ukmon_pylib:$PYTHONPATH python -m metrics.statsToMqtt diskpct=$(df / |tail -1| awk '{print $5 }' | sed 's/%//g') - if [ $diskpct -gt 90 ] ; then + if [ $diskpct -gt 95 ] ; then msg="$(hostname) diskspace is ${diskpct}%" subj="Diskspace alert for $(hostname)" hn="$(hostname)@aws" diff --git a/archive/website/cameraStatusReport.sh b/archive/website/cameraStatusReport.sh index e2923f5fb..537a7b2fe 100644 --- a/archive/website/cameraStatusReport.sh +++ b/archive/website/cameraStatusReport.sh @@ -34,7 +34,7 @@ aws s3 cp $DATADIR/reports/stationlogins.txt $WEBSITEBUCKET/reports/stationlogin cp $TEMPLATES/header.html $DATADIR/reports/statrep.html -echo "

    Camera status report for the network.

    This page provides a status report " >> $DATADIR/reports/statrep.html +echo "

    Camera status report for the network.

    This page provides a status report " >> $DATADIR/reports/statrep.html echo "on the feed of daily data from cameras in the network. RMS cameras are reported red if more than three days " >> $DATADIR/reports/statrep.html echo "late. UFO cameras are reported red if more than 14 days late." >> $DATADIR/reports/statrep.html echo "The date & time are that of the start of the last data capture run recieved." >> $DATADIR/reports/statrep.html diff --git a/archive/website/costReport.sh b/archive/website/costReport.sh index b0aa30606..cd60ec06f 100644 --- a/archive/website/costReport.sh +++ b/archive/website/costReport.sh @@ -47,7 +47,7 @@ v3=$(cat $DATADIR/costs/costs-183798037734-last.csv) lastfullcost=$v3 cp $TEMPLATES/header.html $costfile -echo "

    Daily running costs

    " >> $costfile +echo "

    Daily running costs

    " >> $costfile echo "

    This page shows daily running costs by service of the Archive and Calculation Engine " >> $costfile echo "in USD.
    " >> $costfile echo "The last complete day's cost is \$${lastfullcost}" >> $costfile diff --git a/archive/website/createReportIndex.sh b/archive/website/createReportIndex.sh index 378543409..edf24c4f8 100644 --- a/archive/website/createReportIndex.sh +++ b/archive/website/createReportIndex.sh @@ -28,6 +28,7 @@ else fi cd ${DATADIR}/reports +mkdir -p ${DATADIR}/reports/${fldr} repidx=$fldr/reportindex.js echo "\$(function() {" > $repidx diff --git a/archive/website/createSummaryTable.sh b/archive/website/createSummaryTable.sh index 33577ded1..fab46348f 100644 --- a/archive/website/createSummaryTable.sh +++ b/archive/website/createSummaryTable.sh @@ -1,6 +1,6 @@ #!/bin/bash # Copyright (C) 2018-2023 Mark McIntyre -# Creates the table and bar chart that appear on the website homepage. +# Create the archive website homepage, and the table and bar chart that appear on the page. # Also creates the coverage map, and copies the logfile to the site. # # Parameters diff --git a/archive/website/templates/coverage-maps.html b/archive/website/templates/coverage-maps.html index 66f02c93d..aedfedc8d 100644 --- a/archive/website/templates/coverage-maps.html +++ b/archive/website/templates/coverage-maps.html @@ -77,6 +77,7 @@

    +

    Station Coverage Maps

    To use this page, select a camera or cameras from the dropdown, then select an altitude. diff --git a/archive/website/templates/fbreportindex.html b/archive/website/templates/fbreportindex.html index d9da20b4f..1ec505246 100644 --- a/archive/website/templates/fbreportindex.html +++ b/archive/website/templates/fbreportindex.html @@ -48,7 +48,7 @@

    - +

    The table below shows a list of fireballs and other possibly interesting events.

    The data here are released under the CC BY 4.0 license, so if you are using the data diff --git a/archive/website/templates/frontpage.html b/archive/website/templates/frontpage.html index d08ab16df..c1af50676 100644 --- a/archive/website/templates/frontpage.html +++ b/archive/website/templates/frontpage.html @@ -8,7 +8,7 @@ - + @@ -45,10 +45,15 @@

    -

    Welcome to the UK Meteor data archive, the data archive of the UK Meteor Network.

    +
    +
    +

    Welcome to the data archive of the UK Meteor Network (UKMON).

    This site hosts our data archive. Our main public website is here and you can find us on Social Media here


    +

    + To view our livestream of images from the camera network, click the here or on Live on the menu above. +

    Use the menus above or click a link in the table below the graph to access reports on annual activity, shower activity and individual stations in our network. You can also search for detections matching @@ -58,7 +63,7 @@

    Coverage

    We currently have #NUMCAMS# cameras in the network, the locations of which are shown at the foot of this page. Camera Coverage can be seen here. Camera status can be quickly checked here or - in more detail on the GMN site here.

    + in more detail on the GMN site here.

    Summary Data

    The graph below shows activity year to date and the table summarises our entire dataset. You can also see an excellent map of all detections here, diff --git a/archive/website/templates/reportindex.html b/archive/website/templates/reportindex.html index f25d6ab23..3b5c7d9f4 100644 --- a/archive/website/templates/reportindex.html +++ b/archive/website/templates/reportindex.html @@ -50,7 +50,7 @@

    - +

    The table below provides access to a summary report by year and by shower, and reports on the trajectories and orbits of matched events.

    The Annual and Shower reports provide a summary of the number of meteors detected, diff --git a/archive/website/templates/searchdialog.js b/archive/website/templates/searchdialog.js index a53350829..e85d171cf 100644 --- a/archive/website/templates/searchdialog.js +++ b/archive/website/templates/searchdialog.js @@ -98,6 +98,7 @@ function myFunc(myObj) { for (x in myObj) { console.log(myObj[x]); var dtarr = myObj[x].split(','); + var cams = dtarr[4].replaceAll(";", "; "); if (dtarr[0] > 0 ){ txt +='

  • PresidentCurrently VacantPeter Campbell-Burns
    Secretary
    '; dt = new Date(dtarr[0]*1000); @@ -109,7 +110,7 @@ function myFunc(myObj) { txt += ""; txt += dtarr[3]; txt += ""; - txt += dtarr[4]; + txt += cams; txt += ""; errimg = "onerror=\"this.onerror=null;this.src='/img/missing.png';\""; txt += "\"Image"; diff --git a/archive/website/templates/shwrcsvindex.html b/archive/website/templates/shwrcsvindex.html index 64a4a87e0..8ea0a6659 100644 --- a/archive/website/templates/shwrcsvindex.html +++ b/archive/website/templates/shwrcsvindex.html @@ -48,6 +48,7 @@
    +

    The data here are released under the CC BY 4.0 license, so if you are using the data whether for scientific or other purposes, your must reference this web site in your work. diff --git a/archive/website/templates/statreportindex.html b/archive/website/templates/statreportindex.html index a0fdae7fe..b62bd482a 100644 --- a/archive/website/templates/statreportindex.html +++ b/archive/website/templates/statreportindex.html @@ -46,7 +46,7 @@

    - +

    The table below provides access to a summary report by station.

    The data here are released under the CC BY 4.0 license, so if you are using the data diff --git a/tests/test_apis.py b/tests/test_apis.py index 7c81fe892..4394b5d16 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -172,7 +172,7 @@ def test_getECSV(): apiurl='https://api.ukmeteors.co.uk/getecsv?stat={}&dt={}' res = requests.get(apiurl.format(stationID, dateStr)) assert res.status_code == 200 - assert res.text.split()[-1] == 'time' + assert res.text.split()[-1] == '593.580' stationID = 'UK0080' dateStr = '2024-02-25T20:02:00.1' @@ -188,6 +188,6 @@ def test_getECSVbadDate(): dateStr = '2024-02-25T20:02:' apiurl='https://api.ukmeteors.co.uk/getecsv?stat={}&dt={}' res = requests.get(apiurl.format(stationID, dateStr)) - assert res.status_code == 200 - assert res.text.split()[-1] == 'format' + assert res.status_code == 502 + #assert res.text.split()[-1] == 'format' return diff --git a/usermgmt/README.md b/usermgmt/README.md deleted file mode 100644 index 4bb751c08..000000000 --- a/usermgmt/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# UKMDA User Management - -This folder contains the scripts used to add/maintain new cameras to the network. - -## Basic Principles -A camera consists of an RMS ID, location and pointing direction. - -Each _location_ is assigned a unique *AWS* user with credentials that grant suitable permissions to the S3 storage. For example, a contributor in Toytown might be assigned a unique AWS user "Toytown". - -AWS credentials are not shared between locations. If a location's credentials are compromised, they can be disabled without impacting the rest of the network and new credentials can then be issued. The *ukmon-pitools* toolset is designed so that new credentials will be picked up the next time *refreshTools.sh* is run on the impacted Pi (either at reboot or manually). - -Each _camera_ is allocated a unique *Unix* ID. The public ssh key provided by the operator is used to enable *ukmon-pitools* to login and collect configuration information for the camera. For example if the contributor at Toytown had two cameras facing North and South, the cameras might be named "toytown_s" and "toytown_n". Note that camera names are in lower case, must be unique and that SSH keys should not be shared between cameras. If the operator chooses to share keys they do so at their own risk and it means that if the key is compromised, all their cameras will be unable to connect. - - -# User Management tool -Unix and AWS User creation and configuration are managed by *stationMaint.ps1* which calls a python programme that uses native AWS and Unix libraries to execute the required commands. The tool requires AWS and SSH credentials which are restricted to administrators (see note below). - -## Setup -Prerequisites: -* anaconda or miniconda -* An AWS profile with the usermaintenance IAM permissions. The default profile name is *ukmda_maint*. -* An SSH key thats permissioned to connect to the server. The default key name is *ukmda_admin*. - -To install the app, copy the python files, requirements.txt and powershell script to a folder of your choosing, and update the environment variables in the powershell script as needed. Now run the powershell script which will create a suitable Conda environment, install the requirements and launch the tool. - -## Adding a new camera -To add a new camera the camera operator must supply the following: -* RMS ID (from the RMS .config file) -* town or village (eg Toytown) -* approx pointing direction (eg SW) -* the SSH public key generated by *ukmon-pitools*. -* Human name and email address of the operator. - -RMS ID, pointing direction and SSH key are unique to each camera. Other values will generally be shared -by other cameras at the same location. - -Once the information has been gathered, select Camera/Add and fill in the boxes. Note that the boxes are part-prepopulated with values from whatever row your cursor was on, so that if you're adding another camera at an existing location, you can save a bit of typing. - -## Amending a camera -The location and pointing direction cannot be changed once set. To change these values you'll need to follow the process to move a camera. -All other values can be amended by selecting the line and updating the values. - -## Moving a camera -To move a camera to a new location, select the row containing the camera then select Camera/Move. After you fill in any new information, a new unix and AWS user will be created and the configuration files for the old user will be updated. The old camera must then be marked disabled as explained next. - -IMPORTANT NOTE: the camera owner should NOT make any changes - the system will automatically update their configuration. - -## Disabling a camera -To disable a camera, change the Active column from 1 to today's date in YYYYMMDD format eg 20220715. This removes the camera from current reporting, but retains the details for any historical reporting. - -# Disabling a location -To disable a location, we revoke the corresponding AWS user's keys, and if necessary, delete the AWS user and Unix ID. No other user will be affected. This is not currently managed by the UI as it is a drastic action that we do not want to carry out accidentally! - -# Permissions Needed -This tool requires two sets of permissions: -* an AWS profile with permissions to add/amend/delete users in IAM. This is required to manage the AWS roles. -* an SSH key with permissions to login to the Batch server. This is required to manage the Unix accounts. - -# Parameters -The powershell script lists the parameters that need to be set, such as AWS profile to use, the buckets and so on. The ansible YAML script sets these values. - - -# Copyright -All code Copyright (C) 2018-2023 Mark McIntyre \ No newline at end of file diff --git a/usermgmt/build.ps1 b/usermgmt/build.ps1 deleted file mode 100644 index 325846018..000000000 --- a/usermgmt/build.ps1 +++ /dev/null @@ -1 +0,0 @@ -pyinstaller .\stationMaint2.py --noconsole --onefile --windowed --icon .\camera.ico --clean diff --git a/usermgmt/deploy-local.yml b/usermgmt/deploy-local.yml deleted file mode 100644 index 76677f582..000000000 --- a/usermgmt/deploy-local.yml +++ /dev/null @@ -1,76 +0,0 @@ ---- -- hosts: ukmonhelper2 - gather_facts: no - tasks: - - name: import dev variables - include_vars: ./dev-vars.yml - tags: dev - - name: import prod variables - include_vars: ./prod-vars.yml - tags: prod - - name: Ensures {{windestdir}} exists - file: path={{windestdir}} state=directory - delegate_to: localhost - tags: [dev,prod] - - name: Ensures {{windestdir}}/caminfo exists - file: path={{windestdir}}/caminfo state=directory - delegate_to: localhost - tags: [dev,prod] - - - name: build exe - ansible.builtin.shell: | - conda activate ukmon-admin - cd windows - pyinstaller ./stationMaint2.py --noconsole --onefile --windowed --icon .\camera.ico - args: - executable: pwsh.exe - delegate_to: localhost - register: outdata - tags: [dev,prod] - - - name: print debug - debug: - var: outdata.stdout - - name: Copy files - copy: src={{ item.src }} dest={{ item.dest }} mode={{ item.mode }} - delegate_to: localhost - tags: [dev,prod] - with_items: - - {src: '{{winsrcdir}}/dist/StationMaint2.exe', dest: '{{windestdir}}/', mode: '754', backup: yes, directory_mode: no } - - {src: '{{winsrcdir}}/camera.ico', dest: '{{windestdir}}/', mode: '644', backup: yes, directory_mode: no } - - {src: '{{winsrcdir}}/stationmaint.cfg', dest: '{{windestdir}}/', mode: '644', backup: yes, directory_mode: no } - - name: update source bucket - lineinfile: - path: '{{windestdir}}/stationmaint.cfg' - regexp: 'SRCBUCKET=' - line: 'SRCBUCKET={{srcbucket}}' - delegate_to: localhost - tags: [dev,prod] - - name: update live bucket - lineinfile: - path: '{{windestdir}}/stationmaint.cfg' - regexp: 'LIVEBUCKET=' - line: 'LIVEBUCKET={{livebucket}}' - delegate_to: localhost - tags: [dev,prod] - - name: update web bucket - lineinfile: - path: '{{windestdir}}/stationmaint.cfg' - regexp: 'WEBSITEBUCKET=' - line: 'WEBSITEBUCKET={{websitebucket}}' - delegate_to: localhost - tags: [dev,prod] - - name: update helper ipaddress - lineinfile: - path: '{{windestdir}}/stationmaint.cfg' - regexp: 'HELPERIP=' - line: 'HELPERIP={{serverip}}' - delegate_to: localhost - tags: [dev,prod] - - name: update helper remotedir - lineinfile: - path: '{{windestdir}}/stationmaint.cfg' - regexp: 'REMOTEDIR=' - line: 'REMOTEDIR={{remotedir}}' - delegate_to: localhost - tags: [dev,prod] diff --git a/usermgmt/dev-vars.yml b/usermgmt/dev-vars.yml deleted file mode 100644 index 5dfce2f67..000000000 --- a/usermgmt/dev-vars.yml +++ /dev/null @@ -1,11 +0,0 @@ - winsrcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/usermgmt/windows - windestdir: /mnt/e/dev/meteorhunting/testing/usermgmt - destdir: /home/ec2-user/dev/keymgmt - srcbucket: mjmm-ukmon-shared - livebucket: mjmm-ukmon-live - websitebucket: mjmm-ukmonarchive.co.uk - liveprofile: default - archprofile: ukmda_admin - helper: ukmonhelper2 - serverip: "3.11.55.160" - remotedir: /home/ec2-user/dev/data \ No newline at end of file diff --git a/usermgmt/prod-vars.yml b/usermgmt/prod-vars.yml deleted file mode 100644 index 63b88f793..000000000 --- a/usermgmt/prod-vars.yml +++ /dev/null @@ -1,9 +0,0 @@ - winsrcdir: /mnt/e/dev/meteorhunting/ukmda-dataprocessing/usermgmt/windows - windestdir: /mnt/f/videos/MeteorCam/usermgmt - destdir: /home/ec2-user/keymgmt - srcbucket: ukmda-shared - livebucket: ukmda-live - websitebucket: ukmda-website - helper: ukmonhelper2 - serverip: "3.11.55.160" - remotedir: /home/ec2-user/prod/data \ No newline at end of file diff --git a/usermgmt/server/ukmon.ini b/usermgmt/server/ukmon.ini deleted file mode 100644 index 59c2a1061..000000000 --- a/usermgmt/server/ukmon.ini +++ /dev/null @@ -1,5 +0,0 @@ -# config data for this station -export LOCATION=STATIONLOCATION -export UKMONHELPER=3.11.55.160 -export UKMONKEY=~/.ssh/ukmon -export RMSCFG=~/source/RMS/.config diff --git a/usermgmt/windows/camera.ico b/usermgmt/windows/camera.ico deleted file mode 100644 index 33fcafac1..000000000 Binary files a/usermgmt/windows/camera.ico and /dev/null differ diff --git a/usermgmt/windows/release_notes.md b/usermgmt/windows/release_notes.md deleted file mode 100644 index de9a86dd7..000000000 --- a/usermgmt/windows/release_notes.md +++ /dev/null @@ -1,18 +0,0 @@ -# The UKMON Station Management Tool - -This application is used to add, modify and disable RMS stations linked to UKMON. The app is intended for use only by the UKMON committee members responsible for managing users. - -## Before Installation -First you should contact me, Mark McIntyre. You will need to send me your ssh public key, and i will then create a userid for you on our server and send you the details. - -## Installation -Download the zip file and expand it to a location of your choice, for example `c:\programdata\ukmon` -Create an SSH keypair. -Edit the config file and add the userid i provided, plus the full path to the ssh private key. -Save the config file and run the app. - -## Use -The menus should be pretty self-explanatory. - -## Release Notes -2024.04.2 - first release. \ No newline at end of file diff --git a/usermgmt/windows/requirements.txt b/usermgmt/windows/requirements.txt deleted file mode 100644 index 3894e9b5f..000000000 --- a/usermgmt/windows/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2018-2023 Mark McIntyre -boto3 -openpyxl -pandas -scp -tksheet -paramiko -pyinstaller diff --git a/usermgmt/windows/stationMaint.ps1 b/usermgmt/windows/stationMaint.ps1 deleted file mode 100644 index 28eeee7ef..000000000 --- a/usermgmt/windows/stationMaint.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 2018-2023 Mark McIntyre -push-Location $psscriptroot - -# create conda env if not aleady there -$isadm=(conda env list | select-string "ukmon-admin") -if ($isadm.count -eq 0) { - conda env create -n ukmon-admin python=3.8 - pip install -r requirements.txt -} -conda activate ukmon-admin - -bash -c "rsync -avz --delete ukmonhelper2:keymgmt/sshkeys/ ./sshkeys" -bash -c "rsync -avz --delete ukmonhelper2:keymgmt/sshkeys/ ./keys" -bash -c "rsync -avz --delete ukmonhelper2:keymgmt/sshkeys/ ./csvkeys" -bash -c "rsync -avz --delete ukmonhelper2:keymgmt/sshkeys/ ./inifs" - -python stationMaint2.py -Pop-Location \ No newline at end of file diff --git a/usermgmt/windows/stationMaint2.py b/usermgmt/windows/stationMaint2.py deleted file mode 100644 index 5e5c8a8c8..000000000 --- a/usermgmt/windows/stationMaint2.py +++ /dev/null @@ -1,946 +0,0 @@ -# Copyright (C) 2018-2023 Mark McIntyre - -from tksheet import Sheet -import tkinter as tk -from tkinter import ttk -from tkinter import Frame, Menu -from tkinter import simpledialog -import tkinter.messagebox as tkMessageBox -from tkinter.filedialog import askopenfilename -import boto3 -import os -import sys -import paramiko -import json -import time -import datetime -import tempfile -from scp import SCPClient -from boto3.dynamodb.conditions import Key -import pandas as pd -from configparser import ConfigParser -import logging -import logging.handlers - - -log = logging.getLogger("logger") - - -def loadConfig(cfgdir): - cfgfile = os.path.join(cfgdir, 'stationmaint.cfg') - cfg = ConfigParser() - if not os.path.isfile(cfgfile): - tkMessageBox.showinfo('Warning', f'config file {cfgfile} not found') - exit(0) - cfg.read(cfgfile) - server = cfg['helper']['helperip'] - user = cfg['helper']['user'] - keyfile = cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = c.open_sftp() - try: - handle, tmpfnam = tempfile.mkstemp() - scpcli.get(f'{user}.csv', tmpfnam) - except Exception: - log.warning('unable to find AWS key') - scpcli.close() - c.close() - try: - lis = open(tmpfnam, 'r').readlines() - os.close(handle) - os.remove(tmpfnam) - key, sec = lis[1].split(',') - except Exception: - log.warning('malformed AWS key') - if key: - log.info('retrieved key details') - awskeys = {'key': key.strip(), 'secret': sec.strip()} - else: - awskeys = {} - return cfg, awskeys - - -def addRow(newdata=None, stationid=None, site=None, user=None, email=None, ddb=None, - direction=None, camtype=None, active=None, createdate=None, tblname='camdetails'): - ''' - add a row to the CamDetails table - ''' - if not ddb: - ddb = boto3.resource('dynamodb', region_name='eu-west-2') - if not newdata: - newdata = {'stationid': stationid, 'site': site, 'humanName':user, 'eMail': email, - 'direction': direction, 'camtype': camtype, 'active': active, 'created': createdate, - 'oldcode': stationid} - table = ddb.Table(tblname) - response = table.put_item(Item=newdata) - if response['ResponseMetadata']['HTTPStatusCode'] != 200: - log.info(response) - return - - -def loadLocationDetails(table='camdetails', ddb=None, loadall=False): - if not ddb: - ddb = boto3.resource('dynamodb', region_name='eu-west-2') - table = ddb.Table(table) - res = table.scan() - # strictly, should check for LastEvaluatedKey here, in case there was more than 1MB of data, - # however that equates to around 30,000 users which i hope we never have... - values = res.get('Items', []) - camdets = pd.DataFrame(values) - camdets.sort_values(by=['stationid'], inplace=True) - if not loadall: - camdets.dropna(inplace=True, subset=['eMail','humanName','site']) - camdets['camtype'] = camdets['camtype'].astype(int) - camdets['active'] = camdets['active'].astype(int) - return camdets - - -def findLocationInfo(srchstring, ddb=None, statdets=None): - if statdets is None: - statdets = loadLocationDetails(ddb=ddb) - statdets = statdets[statdets.active==1] - s1 = statdets[statdets.stationid.str.contains(srchstring)] - s2 = statdets[statdets.eMail.str.contains(srchstring)] - s3 = statdets[statdets.humanName.str.contains(srchstring)] - s4 = statdets[statdets.site.str.contains(srchstring)] - srchres = pd.concat([s1, s2, s3, s4]) - srchres.drop(columns=['oldcode','active','camtype'], inplace=True) - return srchres - - -def cameraExists(stationid=None, location=None, direction=None, ddb=None, statdets=None): - if statdets is None: - statdets = loadLocationDetails(ddb=ddb) - statdets = statdets[statdets.active==1] - if stationid: - if len(statdets[statdets.stationid == stationid]) > 0: - return True - if location and direction: - s1 = statdets[statdets.site == location] - if len(s1) > 0: - if len(s1[s1.direction == direction]) > 0: - return True - return False - - -def dumpCamTable(outdir, statdets=None, ddb=None, exportmindets=False): - if statdets is None: - statdets = loadLocationDetails(ddb=ddb) - statdets = statdets[statdets.active==1] - if exportmindets: - statdets = statdets[['stationid', 'eMail']] - statdets.to_csv(os.path.join(outdir,'camtable.csv'), index=False) - - -def getCamUpdateDate(camid, ddb=None): - if not ddb: - ddb = boto3.resource('dynamodb', region_name='eu-west-2') - table = ddb.Table('LiveBrightness') - resp = table.query(KeyConditionExpression=Key('camid').eq(camid), - IndexName = 'camid-CaptureNight-index', - ScanIndexForward=False, - Limit=1, - Select='SPECIFIC_ATTRIBUTES', - ProjectionExpression='CaptureNight') - if len(resp['Items']) > 0: - return int(resp['Items'][0]['CaptureNight']) - else: - return 0 - - -class srcResBox(tk.Toplevel): - ''' - A class for the search dialog - ''' - def __init__(self, title, message): - tk.Toplevel.__init__(self) - self.details_expanded = False - self.title(title) - self.geometry('600x250') - self.minsize(700, 250) - self.maxsize(700, 550) - self.rowconfigure(0, weight=0) - self.rowconfigure(1, weight=1) - self.columnconfigure(0, weight=1) - - button_frame = tk.Frame(self) - button_frame.grid(row=0, column=0, sticky='nsew') - button_frame.columnconfigure(0, weight=1) - button_frame.columnconfigure(1, weight=1) - - text_frame = tk.Frame(self) - text_frame.grid(row=1, column=0, padx=(7, 7), pady=(7, 7), sticky='nsew') - text_frame.rowconfigure(0, weight=1) - text_frame.columnconfigure(0, weight=1) - - self.textbox = tk.Text(text_frame, height=6) - self.textbox.insert('1.0', message) - self.textbox.grid(row=0, column=0, sticky='nsew') - self.geometry('700x550') - self.details_expanded = True - - -class infoDialog(simpledialog.Dialog): - ''' - A class to gather or display info on a camera - ''' - def __init__(self, parent, title, location, user, email, sshkey='', id=''): - self.data = [] - self.data.append(id) - self.data.append(location) - self.data.append('') - self.data.append(user) - self.data.append(email) - self.data.append(sshkey) - self.parent = parent - - super().__init__(parent, title) - - def body(self, frame): - self.camid_label = tk.Label(frame, width=25, text="RMS ID") - self.camid_label.pack() - self.camid_box = tk.Entry(frame, width=25) - self.camid_box.insert(tk.END, self.data[0]) - self.camid_box.pack() - - self.location_label = tk.Label(frame, width=25, text="Location") - self.location_label.pack() - self.location_box = tk.Entry(frame, width=25) - self.location_box.insert(tk.END, self.data[1]) - self.location_box.pack() - - self.direction_label = tk.Label(frame, width=25, text="direction") - self.direction_label.pack() - self.direction_box = tk.Entry(frame, width=25) - self.direction_box.pack() - - self.ownername_label = tk.Label(frame, width=25, text="owner name") - self.ownername_label.pack() - self.ownername_box = tk.Entry(frame, width=25) - self.ownername_box.insert(tk.END, self.data[3]) - self.ownername_box.pack() - - self.email_label = tk.Label(frame, width=25, text="email address") - self.email_label.pack() - self.email_box = tk.Entry(frame, width=25) - self.email_box.insert(tk.END, self.data[4]) - self.email_box.pack() - - self.sshkey_label = tk.Label(frame, width=25, text="SSH key") - self.sshkey_label.pack() - self.sshkey_box = tk.Entry(frame, width=50) - self.sshkey_box.insert(tk.END, self.data[5]) - self.sshkey_box.pack() - - def ok_pressed(self): - self.data[0] = self.camid_box.get().strip() - self.data[1] = self.location_box.get().strip().capitalize() - self.data[2] = self.direction_box.get().strip().upper() - self.data[3] = self.ownername_box.get().strip() - self.data[4] = self.email_box.get().strip() - self.data[5] = self.sshkey_box.get().strip() - if cameraExists(location=self.data[1],direction=self.data[2], statdets=self.parent.stationdetails): - msg = f'{self.data[1]}_{self.data[2]} already exists' - tk.messagebox.showinfo(title="Information", message=msg) - else: - self.destroy() - - def cancel_pressed(self): - self.data[0] = '' - self.destroy() - - def buttonbox(self): - self.ok_button = tk.Button(self, text='OK', width=5, command=self.ok_pressed) - self.ok_button.pack(side="left") - cancel_button = tk.Button(self, text='Cancel', width=5, command=self.cancel_pressed) - cancel_button.pack(side="right") - self.bind("", lambda event: self.ok_pressed()) - self.bind("", lambda event: self.cancel_pressed()) - - -class statOwnerDialog(simpledialog.Dialog): - ''' - A class to display station ownership data - ''' - def __init__(self, parent): - self.stationdetails = parent.stationdetails - self.parent = parent - super().__init__(parent, 'Owner Info') - - def body(self, frame): - columns = ('#1','#2','#3','#4') - tree = ttk.Treeview(frame, columns=columns, show='headings') - tree.heading('#1', text='camid') - tree.heading('#2', text='site') - tree.heading('#3', text='email') - tree.heading('#4', text='humanName') - contacts = [] - for _, li in self.stationdetails.iterrows(): - contacts.append((li['stationid'], li['site'], li['eMail'], li['humanName'])) - for contact in contacts: - tree.insert('', tk.END, values=contact) - tree.grid(row=0, column=0, sticky='nsew') - scrollbar = ttk.Scrollbar(root, orient=tk.VERTICAL, command=tree.yview) - tree.configure(yscroll=scrollbar.set) - scrollbar.grid(row=0, column=1, sticky='ns') - return - - -class CamMaintenance(Frame): - ''' - The main camera maintenance window class - ''' - def __init__(self, parent, cfgdir): - self.parent = parent - Frame.__init__(self, parent) - - self.cfg, awskeys = loadConfig(cfgdir) - self.conn = boto3.Session(aws_access_key_id=awskeys['key'], aws_secret_access_key=awskeys['secret']) - self.bucket_name = self.cfg['store']['srcbucket'] - - os.makedirs('jsonkeys', exist_ok=True) - os.makedirs('csvkeys', exist_ok=True) - os.makedirs('users', exist_ok=True) - os.makedirs('inifs', exist_ok=True) - os.makedirs('sshkeys', exist_ok=True) - self.resyncLocalFiles() - - self.ddb = self.conn.resource('dynamodb', region_name='eu-west-2') - try: - self.stationdetails = loadLocationDetails(ddb=self.ddb) - except Exception: - log.info('unable to get operator details - probably wrong AWS profile') - exit(1) - tmpdf = self.stationdetails[['site','stationid','direction','camtype','active','humanName','eMail','oldcode','created']] - tmpdf = tmpdf.sort_values(by=['active','site','stationid'], ascending=[True,True,True]) - self.data = tmpdf.values.tolist() - self.hdrs = tmpdf.columns.tolist() - - self.parent.title('Station Maintenance') - - # Make menu - self.menuBar = Menu(self.parent) - self.parent.config(menu=self.menuBar) - - # File menu - fileMenu = Menu(self.menuBar, tearoff=0) - fileMenu.add_command(label="Exit", command=self.on_closing) - - camMenu = Menu(self.menuBar, tearoff=0) - camMenu.add_command(label = "Add Camera", command = self.addCamera) - camMenu.add_command(label = "Relocate Camera", command = self.moveCamera) - camMenu.add_command(label = "Deactivate Camera", command = self.delCamera) - camMenu.add_separator() - camMenu.add_command(label = "Remove Location", command = self.delOperator) - camMenu.add_separator() - camMenu.add_command(label = "Download Platepar", command = self.getPlate) - camMenu.add_command(label = "Update platepar", command = self.newPlate) - camMenu.add_separator() - camMenu.add_command(label = "Update SSH Key", command = self.newSSHKey) - camMenu.add_command(label = "Update AWS Key", command = self.newAWSKey) - camMenu.add_separator() - camMenu.add_command(label = "Check Camera", command = self.checkLastUpdate) - - ownMenu = Menu(self.menuBar, tearoff=0) - ownMenu.add_command(label = "View Owner Data", command = self.viewOwnerData) - ownMenu.add_command(label = "Search Data", command = self.searchOwnerData) - - self.menuBar.add_cascade(label="File", underline=0, menu=fileMenu) - self.menuBar.add_cascade(label="Camera", underline=0, menu=camMenu) - self.menuBar.add_cascade(label="Owners", underline=0, menu=ownMenu) - - parent.grid_columnconfigure(0, weight = 1) - parent.grid_rowconfigure(0, weight = 1) - self.frame = tk.Frame(self) - - self.frame.grid_columnconfigure(0, weight = 1) - self.frame.grid_rowconfigure(0, weight = 1) - self.sheet = Sheet(self.parent, - page_up_down_select_row = True, - expand_sheet_if_paste_too_big = True, - #empty_vertical = 0, - column_width = 120, - startup_select = (0,1,"rows"), - data = self.data, - headers = self.hdrs, - height = 700, - width = 700) - - self.sheet.enable_bindings(("single_select", - "drag_select", - "select_all", - "column_width_resize", - "double_click_column_resize", - "row_width_resize", - "column_height_resize", - "arrowkeys", - "row_height_resize", - "double_click_row_resize", - "right_click_popup_menu", - "rc_delete_row", - "copy", - "cut", - "paste", - "delete", - "undo", - "edit_cell" - )) - self.sheet.popup_menu_add_command("Sort by this Column", self.columns_sort) - - self.frame.grid(row = 1, column = 0, sticky = "nswe") - self.sheet.grid(row = 0, column = 0, sticky = "nswe") - - self.sheet.change_theme("light green") - - self.sheet.set_all_column_widths() - - #self.sheet.extra_bindings("end_delete_rows", self.end_delete_rows) - #self.sheet.extra_bindings("column_select", self.column_select) - self.sheet.extra_bindings([("all_select_events", self.all_extra_bindings), - ("begin_edit_cell", self.begin_edit_cell), - ("end_edit_cell", self.end_edit_cell)]) - - self.sheet.popup_menu_add_command('Sort', self.columns_sort, table_menu = False, index_menu = False) - - def hide_columns_right_click(self, event = None): - currently_displayed = self.sheet.display_columns() - exclude = set(currently_displayed[c] for c in self.sheet.get_selected_columns()) - indexes = [c for c in currently_displayed if c not in exclude] - self.sheet.display_columns(indexes = indexes, enable = True, refresh = True) - - def all_extra_bindings(self, event): - pass - - def begin_edit_cell(self, event): - self.oldval = self.data[event[0]][event[1]] - return self.oldval - - def end_edit_cell(self, event): - #log.info(event) - if event[3] != self.oldval: - data = self.data[event[0]] - data[event[1]] = event[3] - newdata = {'stationid': data[1], 'site': data[0], 'humanName':data[5], 'eMail': data[6], - 'direction': data[2], 'camtype': str(data[3]), 'active': int(data[4]), 'oldcode': data[1], - 'created': data[8]} - addRow(newdata, ddb=self.ddb) - return event[3] - - def end_delete_rows(self, event): - pass - - def window_resized(self, event): - pass - - def mouse_motion(self, event): - pass - - def deselect(self, event): - log.info(f'{event} {self.sheet.get_selected_cells()}') - - def rc(self, event): - log.info(event) - - def cell_select(self, response): - pass - - def shift_select_cells(self, response): - pass - - def drag_select_cells(self, response): - pass - - def ctrl_a(self, response): - pass - - def row_select(self, response): - pass - - def shift_select_rows(self, response): - log.info(response) - - def drag_select_rows(self, response): - pass - - def columns_sort(self): - cursel = self.sheet.get_selected_cells() - col = list(cursel)[0][1] - log.info(self.hdrs[col]) - pass - - def column_select(self, response): - pass - - def shift_select_columns(self, response): - pass - - def drag_select_columns(self, response): - pass - - def on_closing(self): - outdir = 'stationdetails' - os.makedirs(outdir, exist_ok=True) - dumpCamTable(outdir=outdir, statdets=self.stationdetails, exportmindets=False) - log.info('quitting') - self.destroy() - self.parent.quit() - self.parent.destroy() - - def delCamera(self): - tk.messagebox.showinfo(title="Information", message='To remove a camera, set Active=current date yyyymmdd') - return - - def delOperator(self): - tk.messagebox.showinfo(title="Information", message='Not implemented yet') - return - - def viewOwnerData(self): - statOwnerDialog(self) - return - - def searchOwnerData(self): - srchstring = simpledialog.askstring("Some_Name", "Search String",parent=root) - srchres = findLocationInfo(srchstring, statdets=self.stationdetails) - msgtext = '' - for _, li in srchres.iterrows(): - msgtext = msgtext + f'{li.stationid:10s}{li.site:20s}{li.eMail:30s}{li.humanName:20s}\n' - srcResBox(title="Results", message=msgtext) - return - - def moveCamera(self): - self.addCopyCamera(move=True) - return - - def checkLastUpdate(self): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - camid = curdata[1] - lastupd = getCamUpdateDate(camid, ddb=self.ddb) - msg = f'{camid} last sent a live image on {lastupd}' - tk.messagebox.showinfo(title="Information", message=msg) - return - - def addCamera(self): - self.addCopyCamera(move=False) - return - - def addCopyCamera(self, move=False): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - user = curdata[5] - email = curdata[6] - if move is True: - sshkey = self.getSSHkey(curdata[0], curdata[2]) - id = curdata[1] - title = 'Move Camera' - oldloc = curdata[0].lower() + '_' + curdata[2].lower() - created = curdata[7] - else: - sshkey = '' - id = '' - title = 'Add Camera' - oldloc = '' - created = datetime.datetime.now().strftime('%Y%m%d') - answer = infoDialog(self, title, curdata[0], user, email, sshkey, id) - if answer.data[0].strip() != '': - d = answer.data - rmsid = str(d[0]).upper() - location = str(d[1]).capitalize() - cameraname = d[1].lower() + '_' + d[2].lower() - with open(os.path.join('sshkeys', cameraname + '.pub'), 'w') as outf: - outf.write(d[5]) - rowdata=[d[1],d[0],d[2],'2','1',d[3],d[4],d[0]] - self.sheet.insert_row(values=rowdata, idx=0) - self.addNewAwsUser(location) - self.createIniFile(cameraname) - self.addNewUnixUser(location, cameraname, oldloc) - self.addNewOwner(rmsid, location, str(d[3]), str(d[4]), str(d[2]), '2','1', created) - log.info('done') - return - - def newSSHKey(self): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - user,email = self.getUserDetails(self.stationdetails, curdata[1]) - sshkey = '' - id = '' - title = 'Update SSH Key' - answer = infoDialog(self, title, curdata[0], user, email, sshkey, id) - if answer.data[0].strip() != '': - d = answer.data - location = str(d[1]).capitalize() - cameraname = d[1].lower() + '_' + d[2].lower() - with open(os.path.join('sshkeys', cameraname + '.pub'), 'w') as outf: - outf.write(d[5]) - self.addNewUnixUser(location, cameraname, updatemode=2) - self.datachanged = True - return - - def getPlate(self): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - ppdir = self.cfg['helper']['platepardir'] - ppdir = os.path.join(ppdir, curdata[1]) - os.makedirs(ppdir, exist_ok=True) - ppfile = 'platepar_cmn2010.cal' - site = curdata[0].capitalize() - camid = curdata[1].upper() - - server = self.cfg['helper']['helperip'] - user='ec2-user' - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = SCPClient(c.get_transport()) - remotedir = self.cfg['helper']['remotedir'] - remotef=f'{remotedir}/consolidated/platepars/{camid}.json' - localf = os.path.join(ppdir, ppfile) - scpcli.get(remotef, localf) - scpcli.close() - c.close() - - s3 = boto3.client('s3') - res = s3.list_objects_v2(Bucket=self.bucket_name, Prefix=f'archive/{site}/{camid}/2023/202312/') - if res['KeyCount'] > 0: - keys=res['Contents'] - fitsfiles = [x['Key'] for x in keys if 'fits' in x['Key']] - fitsfiles.sort() - fitsfiles = fitsfiles[-10:] - for ff in fitsfiles: - _, ffname = os.path.split(ff) - dlf = os.path.join(ppdir, ffname) - s3.download_file(self.bucket_name, ff, dlf) - cfgfiles = [x['Key'] for x in keys if '.config' in x['Key']] - cfgfiles.sort() - dlf = os.path.join(ppdir,'.config') - s3.download_file(self.bucket_name, cfgfiles[-1], dlf) - return - - def newPlate(self): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - ppdir =self.cfg['helper']['platepardir'] - ppdir = os.path.join(ppdir, curdata[1]) - ppfile = 'platepar_cmn2010.cal' - plate = '' - title = 'Select New Platepar File' - plate = askopenfilename(title=title, defaultextension='*.cal',initialdir=ppdir, initialfile=ppfile) - if plate: - self.uploadPlatepar(curdata, plate) - log.info(plate) - return - - def resyncLocalFiles(self): - server = self.cfg['helper']['helperip'] - user='ec2-user' - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = c.open_sftp() - for fldr in ['csvkeys','inifs','keys','sshkeys']: - targdir = f'/home/{user}/keymgmt/{fldr}' - flist = scpcli.listdir_attr(targdir) - for fil in flist: - copyme = True - fname = fil.filename - localfname = os.path.join(fldr, fname) - if os.path.isfile(localfname): - mtime = os.path.getmtime(localfname) - if fil.st_mtime - mtime > 1: - copyme = True - else: - copyme = False - if copyme: - scpcli.get(f'{targdir}/{fname}', localfname) - print(f'Updated {fname}') - scpcli.close() - c.close() - - return - - def newAWSKey(self): - cursel = self.sheet.get_selected_cells() - cr = list(cursel)[0][0] - curdata = self.data[cr] - location = curdata[0] - self.createNewAwsKey(location, self.stationdetails) - - def uploadPlatepar(self, camdets, plateparfile): - server = self.cfg['helper']['helperip'] - user='ec2-user' - uplfile = f'/tmp/platepar_cmn2010_{camdets[1]}.cal' - camname = f'{camdets[0]}_{camdets[3]}'.lower() - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = SCPClient(c.get_transport()) - scpcli.put(plateparfile, uplfile) - command = f'sudo mkdir -p /var/sftp/{camname}/platepar/ && sudo chown {camname}:{camname} /var/sftp/{camname}/platepar' - log.info(f'running {command}') - _, stdout, stderr = c.exec_command(command, timeout=10) - for line in iter(stdout.readline, ""): - log.info(line, end="") - for line in iter(stderr.readline, ""): - log.info(line, end="") - command = f'sudo mv {uplfile} /var/sftp/{camname}/platepar/platepar_cmn2010.cal' - log.info(f'running {command}') - _, stdout, stderr = c.exec_command(command, timeout=10) - for line in iter(stdout.readline, ""): - log.info(line, end="") - for line in iter(stderr.readline, ""): - log.info(line, end="") - command = f'sudo chown {camname}:{camname} /var/sftp/{camname}/platepar/platepar_cmn2010.cal' - log.info(f'running {command}') - _, stdout, stderr = c.exec_command(command, timeout=10) - for line in iter(stdout.readline, ""): - log.info(line, end="") - for line in iter(stderr.readline, ""): - log.info(line, end="") - scpcli.close() - c.close() - return - - def addNewOwner(self, rmsid, location, user, email, direction, camtype, active, created): - log.info(f'adding new owner {user} with {email} for {rmsid} at {location}') - newdata = {'stationid': rmsid, 'site': location, 'humanName':user, 'eMail': email, - 'direction': direction, 'camtype': camtype, 'active': int(active), 'oldcode': rmsid, - 'created': created} - addRow(newdata=newdata, ddb=self.ddb) - return - - def addNewAwsUser(self, location): - log.info(f'adding new location {location} to AWS') - archkeyf = 'jsonkeys/' + location + '_arch.key' - archuserdets = 'users/' + location + '_arch.txt' - archcsvf = os.path.join('csvkeys', location.lower() + '_arch.csv') - os.makedirs('jsonkeys', exist_ok=True) - os.makedirs('csvkeys', exist_ok=True) - os.makedirs('users', exist_ok=True) - - iamc = self.conn.client('iam') - try: - _ = iamc.get_user(UserName=location) - log.info('location exists, not adding it') - archkey = None - except Exception: - log.info('new location') - usr = iamc.create_user(UserName=location) - with open(archuserdets, 'w') as outf: - outf.write(str(usr)) - archkey = iamc.create_access_key(UserName=location) - with open(archkeyf, 'w') as outf: - outf.write(json.dumps(archkey, indent=4, sort_keys=True, default=str)) - with open(archcsvf,'w') as outf: - outf.write('Access key ID,Secret access key\n') - outf.write('{},{}\n'.format(archkey['AccessKey']['AccessKeyId'], archkey['AccessKey']['SecretAccessKey'])) - _ = iamc.add_user_to_group(GroupName='cameras', UserName=location) - if archkey is not None: - self.createKeyFile(location) - return - - def createNewAwsKey(self, location, caminfo): - server = self.cfg['helper']['helperip'] - user='ec2-user' - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - command = f'/home/{user}/keymgmt/updateAwsKey.sh {location} force' - log.info(f'running {command}') - _, stdout, stderr = c.exec_command(command, timeout=10) - for line in iter(stdout.readline, ""): - log.info(line, end="") - for line in iter(stderr.readline, ""): - log.info(line, end="") - log.info('done') - c.close() - return - - - def updateKeyfile(self, caminfo, location): - server = self.cfg['helper']['platepardir'] - user='ec2-user' - keyf = os.path.join('jsonkeys', location + '.key') - currkey = json.load(open(keyf, 'r')) - archcsvf = os.path.join('csvkeys', location.lower() + '_arch.csv') - with open(archcsvf,'w') as outf: - outf.write('Access key ID,Secret access key\n') - outf.write('{},{}\n'.format(currkey['AccessKey']['AccessKeyId'], currkey['AccessKey']['SecretAccessKey'])) - - affectedcamlist = caminfo[caminfo.site==location] - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = SCPClient(c.get_transport()) - # push the raw keyfile - scpcli.put(keyf, 'keymgmt/rawkeys/live/') - scpcli.put(archcsvf, 'keymgmt/rawkeys/csvkeys/') - scpcli.close() - c.close() - for _, cam in affectedcamlist.iterrows(): - cameraname = cam.site.lower() + '_' + cam.site.lower() - keyfile = os.path.join('sshkeys', cameraname + '.pub') - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = cameraname, pkey = k) - scpcli = SCPClient(c.get_transport()) - scpcli.put(archcsvf, '.') - scpcli.close() - c.close() - - - def getSSHkey(self, loc, dir): - server= self.cfg['helper']['helperip'] - user='ec2-user' - tmpdir=os.getenv('TEMP', default='c:/temp') - cameraname = (loc + '_' + dir).lower() - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - c.connect(hostname = server, username = user, pkey = k) - scpcli = SCPClient(c.get_transport()) - tmpfil = os.path.join(tmpdir,'./tmp.txt') - # dont use os.path.join - source is on unix we are on windows! - scpcli.get(f'keymgmt/sshkeys/{cameraname}.pub', tmpfil) - scpcli.close() - c.close() - - with open(tmpfil, 'r') as inf: - lis = inf.readlines() - #os.remove('./tmp.txt') - return lis[0].strip() - - - def getUserDetails(self, stationdetails, camid): - reqdf = stationdetails[stationdetails.stationid == camid] - if len(reqdf) == 0: - return '','' - return reqdf.eMail.iloc[0], reqdf.humanName.iloc[0] - - - def addNewUnixUser(self, location, cameraname, oldcamname='', updatemode=0): - server = self.cfg['helper']['helperip'] - user='ec2-user' - log.info(f'adding new Unix user {cameraname}') - keyfile = self.cfg['helper']['sshkey'] - k = paramiko.RSAKey.from_private_key_file(os.path.expanduser(keyfile)) - c = paramiko.SSHClient() - c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - c.connect(hostname = server, username = user, pkey = k) - except Exception: - c.connect(hostname = server+'.', username = user, pkey = k) - scpcli = SCPClient(c.get_transport()) - scpcli.put(os.path.join('sshkeys', cameraname + '.pub'), 'keymgmt/sshkeys/') - scpcli.put(os.path.join('keys', location.lower() + '.key'), 'keymgmt/keys/') - scpcli.put(os.path.join('csvkeys', location + '.csv'), 'keymgmt/csvkeys/') - scpcli.put(os.path.join('inifs', cameraname + '.ini'), 'keymgmt/inifs/') - command = f'/home/{user}/keymgmt/addSftpUser.sh {cameraname} {location} {updatemode} {oldcamname}' - log.info(f'running {command}') - _, stdout, stderr = c.exec_command(command, timeout=60) - for line in iter(stdout.readline, ""): - log.info(line) - for line in iter(stderr.readline, ""): - log.info(line) - - log.info('done, collecting output') - infname = os.path.join('keymgmt/inifs/',cameraname + '.ini') - outfname = os.path.join('./inifs', cameraname + '.ini') - while os.path.isfile(outfname) is False: - try: - time.sleep(3) - scpcli.get(infname, outfname) - except Exception: - continue - scpcli.close() - c.close() - return - - - def createKeyFile(self, location): - archbucket = self.cfg['store']['srcbucket'] - livebucket = self.cfg['store']['livebucket'] - webbucket = self.cfg['store']['websitebucket'] - - os.makedirs('keys', exist_ok=True) - outf = 'keys/' + location.lower() + '.key' - with open(outf, 'w') as ouf: - ouf.write('export AWS_DEFAULT_REGION=eu-west-1\n') - ouf.write(f'export CAMLOC="{location}"\n') - ouf.write(f'export S3FOLDER="archive/{location}/"\n') - ouf.write(f'export ARCHBUCKET={archbucket}\n') - ouf.write(f'export LIVEBUCKET={livebucket}\n') - ouf.write(f'export WEBBUCKET={webbucket}\n') - ouf.write('export ARCHREGION=eu-west-2\n') - ouf.write('export LIVEREGION=eu-west-1\n') - ouf.write('export MATCHDIR=matches/RMSCorrelate\n') - return - - - def createIniFile(self, cameraname): - helperip = self.cfg['helper']['helperip'] - os.makedirs('inifs', exist_ok=True) - outf = 'inifs/' + cameraname + '.ini' - with open(outf, 'w') as outf: - outf.write('# config data for this station\n') - outf.write(f'export LOCATION={cameraname}\n') - outf.write(f'export UKMONHELPER={helperip}\n') - outf.write('export UKMONKEY=~/.ssh/ukmon\n') - outf.write('export RMSCFG=~/source/RMS/.config\n') - return - - -def log_timestamp(): - """ Returns timestamp for logging. - """ - return datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') - - -if __name__ == '__main__': - # Initialize main window - dir_ = os.getcwd() #os.path.dirname(os.path.realpath(__file__)) - - log.setLevel(logging.INFO) - - os.makedirs(os.path.join(dir_, 'logs'), exist_ok=True) - log_file = os.path.join(dir_, 'logs', log_timestamp() + '.log') - handler = logging.handlers.TimedRotatingFileHandler(log_file, when='D', interval=1) # Log to a different file each day - handler.setLevel(logging.INFO) - - formatter = logging.Formatter(fmt='%(asctime)s-%(levelname)s-%(module)s-line:%(lineno)d - %(message)s', datefmt='%Y/%m/%d %H:%M:%S') - handler.setFormatter(formatter) - log.addHandler(handler) - - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(logging.DEBUG) - formatter = logging.Formatter(fmt='%(asctime)s-%(levelname)s: %(message)s', datefmt='%Y/%m/%d %H:%M:%S') - ch.setFormatter(formatter) - log.addHandler(ch) - - # Log program start - log.info("Program start") - - root = tk.Tk() - app = CamMaintenance(root, dir_) - root.iconbitmap(os.path.join(dir_,'camera.ico')) - root.protocol("WM_DELETE_WINDOW", app.on_closing) - app.mainloop() diff --git a/usermgmt/windows/stationmaint.cfg b/usermgmt/windows/stationmaint.cfg deleted file mode 100644 index 883b6cd0f..000000000 --- a/usermgmt/windows/stationmaint.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[helper] -USER=adm_mark -SSHKEY=~/.ssh/ukmonhelper -HELPERIP=3.11.55.160 -REMOTEDIR=/home/ec2-user/prod/data -PLATEPARDIR= -[store] -SRCBUCKET=ukmda-shared -LIVEBUCKET=ukmda-live -WEBSITEBUCKET=ukmda-website diff --git a/utils/addVidorImg.ps1 b/utils/addVidorImg.ps1 new file mode 100644 index 000000000..7b03fa64d --- /dev/null +++ b/utils/addVidorImg.ps1 @@ -0,0 +1,50 @@ +# Copyright (C) 2018-2023 Mark McIntyre +# +# powershell script to upload an extra video or image of an event, given a trajectory ID + +$loc = get-location +if ($args.count -lt 1) { + write-output "Usage: addVideoorImg.ps1 trajpath" + write-output " First create a folder for the trajectory eg 20241113_192754.345_UK" + write-output " Copy any additional mp4, mov or jpgs into it, then run this script" + exit +}else { + $pth = $args[0] +} +set-location $PSScriptRoot +# load the helper functions +. .\helperfunctions.ps1 +$ini=get-inicontent analysis.ini +$fbfolder = $ini['localdata']['fbfolder'].replace('$HOME',$home) +$fbfolder = $fbfolder + "/fireballs" +set-location $fbfolder +$traj = (split-path -leaf $pth) + +$yr=$traj.Substring(0,4) +$ym=$traj.Substring(0,6) +$ymd=$traj.Substring(0,8) +$pick=$traj.substring(0,15) + '_trajectory.pickle' +$zipf=$traj.substring(0,15) + '.zip' + +wget https://archive.ukmeteors.co.uk/reports/${yr}/orbits/${ym}/${ymd}/${traj}/${pick} -O ${pth}\${pick} + +if ((test-path $pth\jpgs) -eq $false) { mkdir $pth\jpgs } +if ((test-path $pth\mp4s) -eq $false) { mkdir $pth\mp4s } + +move-item $pth\*.jpg $pth\jpgs +move-item $pth\*.mp4 $pth\mp4s + +compress-archive -path $pth\* -DestinationPath $pth\$zipf -force + +$apikey=(Get-Content ~/.ssh/fbuploadkey.txt) +$headers = @{ + 'Content-type' = 'application/zip' + 'Slug' = ${zipf} + 'apikey'= ${apikey} +} +$url = "https://api.ukmeteors.co.uk/fireballfiles?orbitfile=${zipf}" + +invoke-webrequest -uri $url -infile $pth\$zipf -Method PUT -Headers $headers + +Write-Output "uploaded $zipf at $(get-date)" +set-location $loc diff --git a/utils/analysis.ini b/utils/analysis.ini new file mode 100644 index 000000000..3d0d32ee3 --- /dev/null +++ b/utils/analysis.ini @@ -0,0 +1,29 @@ +# Parameters that define your local configuration +# +# +[pylib] +pylib=$HOME/OneDrive/dev/ukmda-dataprocessing/archive/ukmon_pylib + +[localdata] +FBFOLDER=$HOME/OneDrive/pictures/meteors +MTHLYFOLDER=$HOME/OneDrive/pictures/meteors/mthlystacks + +[rms] +RMS_INSTALLED=1 +RMS_LOC=$HOME/OneDrive/dev/RMS +RMS_ENV=RMS + +[wmpl] +WMPL_INSTALLED=1 +WMPL_LOC=$HOME/OneDrive/dev/WesternMeteorPyLib +WMPL_ENV=wmpl + +[gmn] +IDFILE=~/.ssh/gmnanalysis + +# camid to hostname mappings, yours may be different +[stations] +UK0006=uk0006 +UK000F=uk000f +UK002F=uk002f +UK001L=uk001l diff --git a/utils/getBrightest.ps1 b/utils/getBrightest.ps1 new file mode 100644 index 000000000..ba9933b27 --- /dev/null +++ b/utils/getBrightest.ps1 @@ -0,0 +1,41 @@ +# powershell script to get the 30 best detections from the previous night +# for sharing on social media +# note that you will have to go through and delete non-meteors from the images + +# requirements - you must clone WesternMeteorPyLib and ukmda-dataprocessing from github +# to your local development space. I use onedrive\dev for my code - set this location in $codeloc + + +# copyright (c) Mark McIntyre, 2025- + +set-location $PSScriptRoot + +# load the helper functions +. .\helperfunctions.ps1 +$ini=get-inicontent analysis.ini +$bdir = $ini['localdata']['fbfolder'].replace('$HOME',$home) +$bdir = $bdir + "/brightest" +set-location $bdir + +$outdir = $bdir.replace('\','/') + +$wmplloc = $ini['wmpl']['wmpl_loc'].replace('$HOME',$home) +$repdir = $ini['pylib']['pylib'].replace('$HOME',$home) + +$env:pythonpath="$wmplloc" +Push-Location $repdir + +conda activate ukmon-shared + +Write-Output "working..." +if ($args.count -eq 0) { + python -c "from reports.findBestMp4s import getBestNSingles;getBestNSingles(numtoget=30,outdir='$outdir')" +}else{ + $reqdate = $args[0] + python -c "from reports.findBestMp4s import getBestNSingles;getBestNSingles(numtoget=30,outdir='$outdir', reqdate='$reqdate')" +} + +$outdirw = $outdir.replace('/','\') +explorer "$outdirw" + +Pop-Location \ No newline at end of file diff --git a/utils/helperfunctions.ps1 b/utils/helperfunctions.ps1 new file mode 100644 index 000000000..b914e30dc --- /dev/null +++ b/utils/helperfunctions.ps1 @@ -0,0 +1,125 @@ +# Copyright (C) 2018-2023 Mark McIntyre + +# helper functions + +Function Get-IniContent { + <# + .Synopsis + Gets the content of an INI file + + .Description + Gets the content of an INI file and returns it as a hashtable + + .Notes + Author : Oliver Lipkau + Blog : http://oliver.lipkau.net/blog/ + Source : https://github.com/lipkau/PsIni + http://gallery.technet.microsoft.com/scriptcenter/ea40c1ef-c856-434b-b8fb-ebd7a76e8d91 + Version : 1.0 - 2010/03/12 - Initial release + 1.1 - 2014/12/11 - Typo (Thx SLDR) + Typo (Thx Dave Stiff) + + #Requires -Version 2.0 + + .Inputs + System.String + + .Outputs + System.Collections.Hashtable + + .Parameter FilePath + Specifies the path to the input file. + + .Example + $FileContent = Get-IniContent "C:\myinifile.ini" + ----------- + Description + Saves the content of the c:\myinifile.ini in a hashtable called $FileContent + + .Example + $inifilepath | $FileContent = Get-IniContent + ----------- + Description + Gets the content of the ini file passed through the pipe into a hashtable called $FileContent + + .Example + C:\PS>$FileContent = Get-IniContent "c:\settings.ini" + C:\PS>$FileContent["Section"]["Key"] + ----------- + Description + Returns the key "Key" of the section "Section" from the C:\settings.ini file + + .Link + Out-IniFile + #> + + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})] + [Parameter(ValueFromPipeline=$True,Mandatory=$True)] + [string]$FilePath + ) + + Begin + {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} + + Process + { + Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath" + + $ini = @{} + switch -regex -file $FilePath + { + "^\[(.+)\]$" # Section + { + $section = $matches[1] + $ini[$section] = @{} + $CommentCount = 0 + } + "^(;.*)$" # Comment + { + if (!($section)) + { + $section = "No-Section" + $ini[$section] = @{} + } + $value = $matches[1] + $CommentCount = $CommentCount + 1 + $name = "Comment" + $CommentCount + $ini[$section][$name] = $value + } + "(.+?)\s*=\s*(.*)" # Key + { + if (!($section)) + { + $section = "No-Section" + $ini[$section] = @{} + } + $name,$value = $matches[1..2] + $ini[$section][$name] = $value + } + } + Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath" + Return $ini + } + + End + {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} +} + +Function Test-CommandExists +{ + Param ($command) + $oldPreference = $ErrorActionPreference + $ErrorActionPreference = ‘stop’ + try {if(Get-Command $command){RETURN $true}} + Catch {Write-Host “$command does not exist”; RETURN $false} + Finally {$ErrorActionPreference=$oldPreference} +} #end function test-CommandExists + +Function New-TemporaryFolder { + # Make a new folder based upon a TempFileName + $T="$($Env:temp)\tmp$([convert]::tostring((get-random 65535),16).padleft(4,'0')).tmp" + New-Item -ItemType Directory -Path $T +} \ No newline at end of file diff --git a/utils/uploadOrbit.ps1 b/utils/uploadOrbit.ps1 new file mode 100644 index 000000000..a512a129b --- /dev/null +++ b/utils/uploadOrbit.ps1 @@ -0,0 +1,70 @@ +# Copyright (C) 2018-2023 Mark McIntyre +# +# powershell script to create a zip file of a solution and upload it + +$loc = get-location +if ($args.count -lt 1) { + $curdir = get-location +}else { + $curdir = resolve-path $args[0] +} +set-location $PSScriptRoot +# load the helper functions +. .\helperfunctions.ps1 +$ini=get-inicontent analysis.ini +Set-Location $Loc + +Add-Type -AssemblyName System.Windows.Forms + +conda activate $ini['wmpl']['wmpl_env'] +$wmplloc = $ini['wmpl']['wmpl_loc'].replace('$HOME',$home) +$env:pythonpath="$wmplloc;$env:pythonpath" + +write-output "Find the pickle" +$picklefile= (Get-ChildItem $curdir\*.pickle -r).fullname +if ($picklefile -is [Array] -or $picklefile.length -eq 0) { + $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{ InitialDirectory = $curdir; Filter = 'pickles (*.pickle)|*.pickle'; Title='Select orbit pickle' } + $null = $FileBrowser.ShowDialog() + $picklefile = $filebrowser.filename + if ( $picklefile -eq "" ) { + Write-Output "Cancelled" + set-location $loc + exit + } +} +write-output "find the orbit full name" +$srcdir = ((get-item $picklefile).directoryname).replace('\','/') +$pickname = (get-item $picklefile).name +$origdir = (python -c "from wmpl.Utils.Pickling import loadPickle;pick = loadPickle('${srcdir}','${pickname}');print(pick.output_dir)") +$orbname = split-path $origdir -leaf +if ((test-path $curdir\$orbname) -eq 0 ) +{ + mkdir -force $curdir\$orbname + move-item $picklefile $curdir\$orbname\ +} +$picklefile = (Get-ChildItem $picklefile -r).fullname + +write-output "look for jpgs and mp4s" +mkdir -force $curdir\jpgs +$jpgdir = resolve-path $curdir\jpgs +Move-Item $curdir\*.jpg $jpgdir +mkdir -force $curdir\mp4s +$mp4dir = resolve-path $curdir\mp4s +Move-Item $curdir\*.mp4 $mp4dir + +write-output "Creating zip file" +if ((test-path $env:temp\$orbname\mp4s) -eq 0 ) {mkdir $env:temp\$orbname\mp4s | out-null} +if ((test-path $env:temp\$orbname\jpgs) -eq 0 ) {mkdir $env:temp\$orbname\jpgs | out-null} +copy-item $picklefile $env:temp\$orbname +copy-item $jpgdir\*.jpg $env:temp\$orbname\jpgs +copy-item $mp4dir\*.mp4 $env:temp\$orbname\mp4s +if ((test-path $env:temp\$orbname.zip) -eq 1 ) { remove-item $env:temp\$orbname.zip} +compress-archive -destinationpath "$env:temp\$orbname.zip" -path "$env:temp\$orbname\*" +remove-item $env:temp\$orbname\* -Recurse + +write-output "Uploading $orbname.zip" +curl -X PUT -H "Content-Type:application/zip" --data-binary "@$env:temp\$orbname.zip" "https://api.ukmeteors.co.uk/fireballfiles?orbitfile=$orbname.zip" +move-item $env:temp\$orbname.zip $curdir -Force +Write-Output "uploaded at $(get-date)" +set-location $loc +conda deactivate